@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,387 @@
1
+ import { Arg, Args, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'
2
+
3
+ import { MES_COMMANDS, nextCommandState } from '@operato/ops-contract'
4
+
5
+ import { randomUUID } from 'node:crypto'
6
+
7
+ import { ListParam, ScalarObject, getQueryBuilderFromListParams, getRepository } from '@things-factory/shell'
8
+
9
+ import { askAdvisor } from './actuation-advisor.js'
10
+ import { approvalGateway } from './approval-gateway.js'
11
+ import { approve, dispatch, linkApproval, propose, reject } from './command-dispatcher.js'
12
+ import { actuationReadinessOf, actuationTargetOf, routeActuation } from '../reference/actuation-routing.js'
13
+
14
+ import { actuationOutcomeOf } from './actuation-outcome.js'
15
+ import { databaseCommandStore } from './command-store.js'
16
+ import { TwinCommandRecord } from './twin-command.js'
17
+ import { twinNow } from './twin-now.js'
18
+
19
+ /*
20
+ * ์Šน์ธ ๋Œ€๊ธฐ ์ค„๊ณผ ๊ทธ ๊ฒฐ์ • โ€” **์‚ฌ๋žŒ์ด ์“ฐ๋Š” ์ž๋ฆฌ**.
21
+ *
22
+ * โ”€โ”€ ์™œ ํ•„์š”ํ•œ๊ฐ€ (2026-08-31) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
23
+ * ๊ฒŒ์ดํŠธ๋ฅผ ์„ธ์šฐ๊ณ  ์Šน์ธํ•  ํ™”๋ฉด์„ ๋งŒ๋“ค์ง€ ์•Š์•˜๋‹ค. ๊ทธ๋ž˜์„œ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†๋Š” ์กฐ์น˜๋Š” ์ „๋ถ€ ๋Œ€๊ธฐ ์ƒํƒœ๋กœ ์Œ“์ด๊ณ 
24
+ * **์‚ฌ๋žŒ์ด ์Šน์ธํ•  ๋ฐฉ๋ฒ•์ด ์—†์—ˆ๋‹ค.** ์„œ๋ฒ„๋งŒ ์žˆ๊ณ  ์‚ฌ๋žŒ์ด ์“ธ ์ž๋ฆฌ๊ฐ€ ์—†๋Š” ์ƒํƒœ๋‹ค.
25
+ *
26
+ * โ”€โ”€ ์™œ ์กฐํšŒ์™€ ๊ฒฐ์ •์„ ํ•œ ํŒŒ์ผ์— ๋‘๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
27
+ * ๋Œ€๊ธฐ ์ค„์„ ์—ฌ๋Š” ๊ฒƒ๊ณผ ๊ทธ ์ค„์—์„œ ๊ฒฐ์ •ํ•˜๋Š” ๊ฒƒ์€ ํ•œ ํ™”๋ฉด์˜ ์ผ์ด๋‹ค. ๋‚˜๋ˆ„๋ฉด ํ™”๋ฉด์ด ๋‘ ๊ณณ์„ ๋ถ€๋ฅด๊ฒŒ ๋˜๊ณ ,
28
+ * ๊ทธ์ค‘ ํ•˜๋‚˜๋งŒ ๊ถŒํ•œ์ด ๋ฐ”๋€Œ๋Š” ๋‚  ๋ชฉ๋ก์€ ๋ณด์ด๋Š”๋ฐ ๊ฒฐ์ •์ด ์•ˆ ๋˜๊ฑฐ๋‚˜ ๊ทธ ๋ฐ˜๋Œ€๊ฐ€ ๋œ๋‹ค.
29
+ */
30
+
31
+ @Resolver()
32
+ export class TwinCommandResolver {
33
+ /**
34
+ * ์ปค๋งจ๋“œ ๋ชฉ๋ก โ€” ๊ธฐ๋ณธ์€ **๊ฒฐ์ •์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๊ฒƒ๋ถ€ํ„ฐ**.
35
+ *
36
+ * ์ •๋ ฌ์„ ์ตœ๊ทผ์ˆœ์œผ๋กœ ๋‘์ง€ ์•Š๋Š”๋‹ค. ์Šน์ธ ๋Œ€๊ธฐ ์ค„์—์„œ ์‚ฌ๋žŒ์ด ์ฐพ๋Š” ๊ฒƒ์€ ใ€Œ์ƒˆ๋กœ ์˜จ ๊ฒƒใ€์ด ์•„๋‹ˆ๋ผ
37
+ * **ใ€Œ์˜ค๋ž˜ ๊ธฐ๋‹ค๋ฆฐ ๊ฒƒใ€**์ด๋‹ค โ€” ์˜ค๋ž˜ ๊ธฐ๋‹ค๋ฆฐ ์กฐ์น˜๊ฐ€ ๋’ค๋กœ ๋ฐ€๋ฆฌ๋ฉด ๊ทธ๊ฒƒ์ด ๊ฐ€์žฅ ๋Šฆ๊ฒŒ ์ฒ˜๋ฆฌ๋œ๋‹ค.
38
+ */
39
+ @Query(returns => ScalarObject, {
40
+ nullable: true,
41
+ description: 'List actuation commands with the standard list contract. Defaults to the oldest waiting first.'
42
+ })
43
+ async twinCommands(
44
+ /* ํ‘œ์ค€ ๋ชฉ๋ก ๊ณ„์•ฝ์€ `@Args` ๋กœ ํŽผ์ณ ๋ฐ›๋Š”๋‹ค โ€” `@Arg` ๋กœ ๋ฐ›์œผ๋ฉด ์Šคํ‚ค๋งˆ ์กฐ๋ฆฝ์ด ์‹คํŒจํ•œ๋‹ค. */
45
+ @Args(type => ListParam) params: ListParam,
46
+ @Ctx() context: ResolverContext
47
+ ): Promise<any> {
48
+ const domainId = context.state.domain?.id
49
+ if (!domainId) return { items: [], total: 0 }
50
+
51
+ const effective: ListParam = {
52
+ ...(params ?? ({} as ListParam)),
53
+ sortings: params?.sortings?.length ? params.sortings : [{ name: 'createdAt', desc: false }]
54
+ }
55
+
56
+ const qb = getQueryBuilderFromListParams({
57
+ repository: getRepository(TwinCommandRecord),
58
+ params: effective,
59
+ domain: context.state.domain,
60
+ alias: 'c',
61
+ /* ์ „๋ถ€ ์‹ค์ปฌ๋Ÿผ์ด๋‹ค โ€” ์งˆ์˜ ๋นŒ๋”๋Š” ์—†๋Š” ์ปฌ๋Ÿผ์˜ LIKE ๋ฅผ ๊ฒฝ๊ณ  ํ›„ ๋ฌด์‹œํ•œ๋‹ค(์ธ๋ฑ์Šค ์—†๋Š” ์ „์ฒด ์Šค์บ” ๋ฐฉ์ง€). */
62
+ searchables: ['instanceId', 'type', 'origin', 'state', 'approvalRef', 'decidedBy', 'error'],
63
+ sortables: ['createdAt', 'updatedAt', 'state', 'type', 'instanceId'],
64
+ /* ์‚ฌ๋žŒ์ด ์ฝ๋Š” ์†๋„๋กœ ๋ณธ๋‹ค. */
65
+ defaultLimit: 50,
66
+ maxLimit: 200
67
+ })
68
+
69
+ const [items, total] = await qb.getManyAndCount()
70
+ return { items, total }
71
+ }
72
+
73
+ /**
74
+ * **์ด ์กฐ์น˜์— ๋Œ€ํ•ด ํŠธ์œˆ์ด ์ง€๊ธˆ ๋ณด๋Š” ๊ฒƒ๊ณผ AI ์˜๊ฒฌ** โ€” ์Šน์ธ ํ™”๋ฉด์ด ๋ถ€๋ฅธ๋‹ค.
75
+ *
76
+ * โ”€โ”€ ์™œ ์กฐํšŒ์ธ๊ฐ€ (2026-09-01) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
77
+ * ๊ฒฐ์žฌ์„ ์— ์˜ฌ๋ฆด ๋•Œ ๋ฌป์ง€ ์•Š๋Š”๋‹ค. ๊ทธ ์ž๋ฆฌ๋Š” ํŠธ๋žœ์žญ์…˜ ์•ˆ์ด๊ณ  ๊ฑฐ๊ธฐ์„œ LLM ์„ ๊ธฐ๋‹ค๋ฆฌ๋ฉด DB ์—ฐ๊ฒฐ ํ•˜๋‚˜๋ฅผ ๋ช‡ ์ดˆ
78
+ * ๋ถ™์žก๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ณ  **์Šน์ธํ•˜๋Š” ์‚ฌ๋žŒ์€ ์ง€๊ธˆ ํŒ๋‹จํ•œ๋‹ค** โ€” ์˜ฌ๋ฆฐ ์‹œ๊ฐ์˜ ์˜๊ฒฌ์€ ๋‚ก์•˜๋‹ค.
79
+ *
80
+ * AI ๋Š” ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ด ๊ฐ’์„ ์ฝ์–ด ์Šน์ธยท๊ฑฐ์ ˆ์„ ๋ง‰๋Š” ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค. ๋ฐ˜๋Œ€ํ•ด๋„ ์Šน์ธ๋˜๊ณ  ๋™์˜ํ•ด๋„
81
+ * ๊ฑฐ์ ˆ๋œ๋‹ค โ€” ํŒ์ •์— ์“ฐ๋ฉด AI ๊ฐ€ ์‚ฌ์‹ค์ƒ ์Šน์ธ์ž๊ฐ€ ๋˜๊ณ , ๊ทธ๋Ÿฌ๋ฉด ์‚ฌ๊ณ  ๋’ค์— ์ฑ…์ž„์„ ๋ฌผ์„ ์ž๋ฆฌ๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค.
82
+ */
83
+ @Query(returns => ScalarObject, {
84
+ nullable: true,
85
+ description: 'What the twin sees now for one command, plus an AI opinion when asked. Advisory only โ€” never gates the decision.'
86
+ })
87
+ async twinActuationOpinion(
88
+ @Arg('id') id: string,
89
+ /**
90
+ * AI ์—๊ฒŒ๋„ ๋ฌผ์„ ๊ฒƒ์ธ๊ฐ€ โ€” **๋ˆ„๋ฅด๋ฉด ๋ฌป๋Š”๋‹ค.**
91
+ *
92
+ * ๊ธฐ๋ณธ์€ ์•ˆ ๋ฌป๋Š”๋‹ค. ํ™”๋ฉด์„ ์—ด ๋•Œ๋งˆ๋‹ค ๋ถ€๋ฅด๋ฉด ์Šน์ธ์ž ๋‹ค์„ฏ ๋ช…์ด ๋‘ ๋ฒˆ์”ฉ ์—ด ๋•Œ ํ•œ ๊ฑด์— ์—ด ๋ฒˆ์ด๊ณ ,
93
+ * ๋Œ€๋ถ€๋ถ„์˜ ์ง€์‹œ๋Š” ํ‰๋ฒ”ํ•ด์„œ ๊ทธ์ค‘ ์•„ํ™‰ ๋ฒˆ์€ ๊ฐ™์€ ๋ง์„ ๋‹ค์‹œ ๋ฐ›๋Š” ๊ฒƒ์ด๋‹ค.
94
+ */
95
+ @Arg('askAi', { nullable: true }) askAi: boolean,
96
+ @Arg('locale', { nullable: true }) locale: string,
97
+ @Ctx() context: ResolverContext
98
+ ): Promise<any> {
99
+ const domainId = context.state.domain?.id
100
+ if (!domainId) throw new Error('ํ…Œ๋„ŒํŠธ๋ฅผ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค')
101
+
102
+ const command = await databaseCommandStore().load(domainId, id)
103
+ if (!command) throw new Error(`๋ชจ๋ฅด๋Š” ์กฐ์น˜์ž…๋‹ˆ๋‹ค (${id})`)
104
+
105
+ /* ํŠธ์œˆ์˜ ์ง€๊ธˆ ์ƒํƒœ๋Š” ๋Š˜ ์ƒˆ๋กœ ์ฝ๋Š”๋‹ค โ€” ๊ณ„์‚ฐ๊ฐ’์ด๋ผ ์‹ธ๊ณ , ์Šน์ธ์ž๋Š” ์ง€๊ธˆ์„ ๋ด์•ผ ํ•œ๋‹ค. */
106
+ const now = twinNow(domainId, command.instanceId, command.causeRef)
107
+
108
+ if (!askAi) return { command, twinNow: now }
109
+
110
+ /*
111
+ * **์˜๊ฒฌ์„ ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค.**
112
+ *
113
+ * ์˜๊ฒฌ์€ ใ€Œ์ง€๊ธˆใ€์— ๋Œ€ํ•œ ๊ฒƒ์ด๊ณ  ์Šน์ธ์ž๋งˆ๋‹ค ์—ฌ๋Š” ์‹œ๊ฐ์ด ๋‹ค๋ฅด๋‹ค. ์ €์žฅํ•ด์„œ ๊ฐ™์€ ๋ฌธ์žฅ์„ ๋Œ๋ ค์ฃผ๋ฉด ๊ทธ๊ฒƒ์ด
114
+ * ์˜คํžˆ๋ ค ๋‚ก์€ ๊ฐ’์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ๊ฐ’์„ ์ฝ์–ด ์Šน์ธยท๊ฑฐ์ ˆ์„ ๋ง‰๋Š” ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค โ€” ํŒ์ •์— ์“ฐ๋ฉด AI ๊ฐ€ ์‚ฌ์‹ค์ƒ
115
+ * ์Šน์ธ์ž๊ฐ€ ๋˜๊ณ , ์‚ฌ๊ณ  ๋’ค์— ์ฑ…์ž„์„ ๋ฌผ์„ ์ž๋ฆฌ๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค.
116
+ */
117
+ const advice = await askAdvisor({ command, twinNow: now, ...(locale ? { locale } : {}) })
118
+
119
+ return { command, twinNow: now, advice }
120
+ }
121
+
122
+ /**
123
+ * ์กฐ์น˜๋ฅผ ๋‚ธ๋‹ค โ€” **๋Œ€๊ธฐ ์ค„์— ๋ฌด์–ธ๊ฐ€๊ฐ€ ๋“ค์–ด์˜ค๋Š” ์œ ์ผํ•œ ์ž๋ฆฌ.**
124
+ *
125
+ * ๊ฒฐ์žฌ์„ ์ด ๋“ฑ๋ก๋œ ๋ฐฐํฌ์—์„œ๋Š” ์—ฌ๊ธฐ์„œ ๋ฐ”๋กœ ๊ฒฐ์žฌ์„ ์— ์˜ฌ๋ผ๊ฐ„๋‹ค(ยง`ApprovalGateway`). ๋“ฑ๋ก๋˜์ง€ ์•Š์•˜์œผ๋ฉด
126
+ * ์ œ์•ˆ ์ƒํƒœ๋กœ ๋‚จ๊ณ , ์ด ๋ฆฌ์กธ๋ฒ„์˜ ์ง์ ‘ ์Šน์ธ์ด ์œ ์ผํ•œ ๋ฌธ์ด ๋œ๋‹ค. ์–ด๋А ์ชฝ์ธ์ง€๋Š” ๋‹ต์— ์ ์–ด ์ค€๋‹ค โ€” ํ™”๋ฉด์ด
127
+ * ใ€Œ์–ด๋””์„œ ์Šน์ธํ•˜๋Š”๊ฐ€ใ€๋ฅผ ์‚ฌ๋žŒ์—๊ฒŒ ๋งํ•ด์•ผ ํ•œ๋‹ค.
128
+ *
129
+ * ๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ๋Š” ์กฐ์น˜๋Š” ๊ฒฐ์žฌ์„ ์— ์˜ฌ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค. ๊ณ„์•ฝ์ด ๊ทธ ํŒ๋‹จ์„ ๋“ ๋‹ค(ยง`requiresApproval`).
130
+ */
131
+ @Directive('@transaction')
132
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
133
+ @Mutation(returns => ScalarObject, { description: 'Propose one actuation command against a twin. Raises it for approval when an approval gateway is installed.' })
134
+ async proposeTwinCommand(
135
+ @Arg('instanceId') instanceId: string,
136
+ @Arg('type') type: string,
137
+ @Arg('payload', type => ScalarObject, { nullable: true }) payload: any,
138
+ @Ctx() context: ResolverContext
139
+ ): Promise<any> {
140
+ const { domain, user } = context.state
141
+ const by = String(user?.email ?? user?.name ?? '').trim()
142
+ if (!by) throw new Error('๋ˆ„๊ฐ€ ๋‚ด๋Š”์ง€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค โ€” ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค')
143
+
144
+ const store = databaseCommandStore(context.state.tx)
145
+ const command = await propose(store, domain.id, MES_COMMANDS, {
146
+ id: randomUUID(),
147
+ instanceId,
148
+ type,
149
+ /*
150
+ * ์‚ฌ๋žŒ์ด ๋‚ธ ๊ฒƒ์ด๋‹ค. ๊ทœ์น™ยทAI ๊ฐ€ ๋‚ด๋Š” ๊ธธ์€ ์•„์ง ์—†๋‹ค โ€” ๊ทธ ๊ธธ์€ ๊ทธ๊ฒƒ์„ ๋‚ด๋Š” ์ชฝ์ด ๋ถ™์ธ๋‹ค.
151
+ * ์—ฌ๊ธฐ์„œ ์ธ์ž๋กœ ๋ฐ›์œผ๋ฉด ์‚ฌ๋žŒ์ด ใ€Œ๊ทœ์น™์ด ๋ƒˆ๋‹คใ€๋กœ ์ ์–ด ์Šน์ธ์ž๋ฅผ ์˜คํŒํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
152
+ */
153
+ origin: 'operator',
154
+ /* ๋‚ธ ์‚ฌ๋žŒ์„ ์ ๋Š”๋‹ค โ€” ์ด ๊ฐ’์ด ์žˆ์–ด์•ผ ์ž๊ธฐ ๊ฒƒ์„ ์ž๊ธฐ๊ฐ€ ์Šน์ธํ•˜๋Š” ๊ฒƒ์„ ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค. */
155
+ proposedBy: by,
156
+ proposedAt: new Date().toISOString(),
157
+ ...(payload ? { payload } : {})
158
+ })
159
+
160
+ if (command.reversible === true) {
161
+ return { command, gateway: null, note: '๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ๋Š” ์กฐ์น˜์ž…๋‹ˆ๋‹ค โ€” ์Šน์ธ ์—†์ด ๋„˜๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค' }
162
+ }
163
+
164
+ const gateway = approvalGateway()
165
+ if (!gateway) {
166
+ return {
167
+ command,
168
+ gateway: null,
169
+ note: '๊ฒฐ์žฌ์„ ์ด ์„ค์น˜๋˜์ง€ ์•Š์€ ๋ฐฐํฌ์ž…๋‹ˆ๋‹ค โ€” ์ด ๋ชฉ๋ก์—์„œ ์ง์ ‘ ์Šน์ธํ•ฉ๋‹ˆ๋‹ค'
170
+ }
171
+ }
172
+
173
+ /*
174
+ * ์˜ฌ๋ฆฌ๊ธฐ์™€ ์ ๊ธฐ๊ฐ€ ํ•œ ํŠธ๋žœ์žญ์…˜์— ์žˆ๋‹ค(`@transaction`). ์˜ฌ๋ ธ๋Š”๋ฐ ์ ์ง€ ๋ชปํ•˜๋ฉด ๊ฒฐ์žฌ์„ ์—๋Š” ์˜ฌ๋ผ๊ฐ€ ์žˆ๊ณ 
175
+ * ์ปค๋งจ๋“œ๋Š” ์ง์ ‘ ๊ฒฐ์ •๋„ ๋˜๋Š” ์ƒํƒœ๊ฐ€ ๋‚จ๋Š”๋‹ค โ€” ๊ทธ๋•Œ ์Šน์ธ์ด ๋‘ ๊ณณ์— ๊ฐˆ๋ฆฐ๋‹ค.
176
+ */
177
+ const approvalRef = await gateway.request(command, context)
178
+ const linked = await linkApproval(store, domain.id, command.id, approvalRef)
179
+
180
+ return { command: linked, gateway: gateway.label, note: `${gateway.label} ์— ์˜ฌ๋ ธ์Šต๋‹ˆ๋‹ค โ€” ์Šน์ธ์€ ๊ทธ๊ณณ์—์„œ ํ•ฉ๋‹ˆ๋‹ค` }
181
+ }
182
+
183
+ /**
184
+ * **์‚ฌ๋žŒ์ด ๊ฒฐ์žฌ์„ ์— ์˜ฌ๋ฆฐ๋‹ค** โ€” ๊ทœ์น™์ด ๋‚ธ ์กฐ์น˜๊ฐ€ ์Šน์ธ ์•ž์— ์„œ๋Š” ์œ ์ผํ•œ ๊ธธ.
185
+ *
186
+ * โ”€โ”€ ์™œ ๊ทœ์น™์ด ์ง์ ‘ ์˜ฌ๋ฆฌ์ง€ ์•Š๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
187
+ * ์—…๋ฌด ๋ชฉ๋ก์˜ ๊ฒฐ์žฌ์„ ์€ **์˜ฌ๋ฆฐ ์‚ฌ๋žŒ ๊ธฐ์ค€**์œผ๋กœ ํ’€๋ฆฐ๋‹ค โ€” `Myself` ยท `MyDepartment` ยท `MySupervisor` ๊ฐ€
188
+ * ์ „๋ถ€ ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ์„ ๋ณธ๋‹ค. ๋ฐฐ๊ฒฝ ๋ฃจํ”„์—๋Š” ใ€Œ๋‚˜ใ€๊ฐ€ ์—†์œผ๋ฏ€๋กœ, ์‹œ์Šคํ…œ ์‚ฌ์šฉ์ž๋ฅผ ์ง€์–ด๋‚ด ์˜ฌ๋ฆฌ๋ฉด ๊ทธ ์…‹์ด
189
+ * ์‹œ์Šคํ…œ ์‚ฌ์šฉ์ž๋ฅผ ๊ฐ€๋ฆฌํ‚ค๊ณ  ๊ฒฐ์žฌ์„ ์ด ๋œป์„ ์žƒ๋Š”๋‹ค.
190
+ *
191
+ * ๊ทธ๋ž˜์„œ ๊ทœ์น™์€ ๋‚ด๋Š” ๊ฒƒ๊นŒ์ง€ ํ•˜๊ณ , ์—ฌ๊ธฐ์„œ ์‚ฌ๋žŒ์ด ์˜ฌ๋ฆฐ๋‹ค. **์˜ฌ๋ฆฐ ์‚ฌ๋žŒ์ด ใ€Œ์ด๊ฒƒ์„ ์Šน์ธ์ž๋“ค ์•ž์— ๋†“์•˜๋‹คใ€์˜
192
+ * ์ฃผ์ฒด**๊ฐ€ ๋˜๊ณ , ๊ทธ ์‚ฌ๋žŒ ๊ธฐ์ค€์œผ๋กœ ๊ฒฐ์žฌ์„ ์ด ํ’€๋ฆฐ๋‹ค.
193
+ *
194
+ * ์˜ฌ๋ฆฐ ์‚ฌ๋žŒ์€ ์ž๊ธฐ๊ฐ€ ์˜ฌ๋ฆฐ ๊ฒƒ์„ ์Šน์ธํ•  ์ˆ˜ ์žˆ๋‹ค โ€” ๊ทœ์น™์ด ๋‚ธ ์กฐ์น˜์—๋Š” ๋‚ธ ์‚ฌ๋žŒ์ด ์—†์œผ๋ฏ€๋กœ ์Šน์ธ์ž๊ฐ€ ๊ณง
195
+ * ๋‘ ๋ฒˆ์งธ ์‚ฌ๋žŒ์ด๋‹ค. ์˜ฌ๋ฆฐ ๊ฒƒ๊ณผ ๋‚ธ ๊ฒƒ์€ ๋‹ค๋ฅด๋‹ค.
196
+ */
197
+ @Directive('@transaction')
198
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
199
+ @Mutation(returns => ScalarObject, { description: 'Raise one proposed command to the approval line. The signed-in user is the submitter.' })
200
+ async raiseTwinCommand(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
201
+ const { domain, user } = context.state
202
+ if (!user?.id) throw new Error('๋ˆ„๊ฐ€ ์˜ฌ๋ฆฌ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค โ€” ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค')
203
+
204
+ const store = databaseCommandStore(context.state.tx)
205
+ const command = await store.load(domain.id, id)
206
+ if (!command) throw new Error(`๋ชจ๋ฅด๋Š” ์กฐ์น˜์ž…๋‹ˆ๋‹ค (${id})`)
207
+
208
+ if (command.approvalRef) {
209
+ throw new Error(`์ด๋ฏธ ๊ฒฐ์žฌ์„ ์— ์˜ฌ๋ผ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค (${command.approvalRef}) โ€” ์Šน์ธ์€ ์—…๋ฌด ๋ชฉ๋ก์—์„œ ํ•ฉ๋‹ˆ๋‹ค`)
210
+ }
211
+ if (command.state !== 'proposed') {
212
+ throw new Error(`${command.state} ์ƒํƒœ์˜ ์กฐ์น˜๋Š” ์˜ฌ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค โ€” ์ด๋ฏธ ๊ฒฐ์ •๋˜์—ˆ๊ฑฐ๋‚˜ ๋‚˜๊ฐ”์Šต๋‹ˆ๋‹ค`)
213
+ }
214
+
215
+ const gateway = approvalGateway()
216
+ if (!gateway) {
217
+ throw new Error('๊ฒฐ์žฌ์„ ์ด ์„ค์น˜๋˜์ง€ ์•Š์€ ๋ฐฐํฌ์ž…๋‹ˆ๋‹ค โ€” ์ด ๋ชฉ๋ก์—์„œ ์ง์ ‘ ์Šน์ธํ•ฉ๋‹ˆ๋‹ค')
218
+ }
219
+
220
+ const approvalRef = await gateway.request(command, context)
221
+ const linked = await linkApproval(store, domain.id, command.id, approvalRef)
222
+
223
+ return { command: linked, gateway: gateway.label, note: `${gateway.label} ์— ์˜ฌ๋ ธ์Šต๋‹ˆ๋‹ค โ€” ์Šน์ธ์€ ๊ทธ๊ณณ์—์„œ ํ•ฉ๋‹ˆ๋‹ค` }
224
+ }
225
+
226
+ /**
227
+ * ์Šน์ธํ•œ๋‹ค โ€” **๋ˆ„๊ฐ€ ์Šน์ธํ–ˆ๋Š”์ง€๋Š” ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ์—์„œ ์˜จ๋‹ค.**
228
+ *
229
+ * ์ธ์ž๋กœ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค. ๋ฐ›์œผ๋ฉด ๋‹ค๋ฅธ ์‚ฌ๋žŒ ์ด๋ฆ„์œผ๋กœ ์Šน์ธํ•  ์ˆ˜ ์žˆ๊ณ , ๊ทธ๋Ÿฌ๋ฉด ๊ทธ ๊ธฐ๋ก์ด ์‚ฌ๊ณ  ๋’ค์— ์“ธ ์ˆ˜
230
+ * ์—†์–ด์ง„๋‹ค. ์Šน์ธ ๊ธฐ๋ก์˜ ๊ฐ’์–ด์น˜๊ฐ€ ๊ฑฐ๊ธฐ์„œ ๋‚˜์˜จ๋‹ค.
231
+ */
232
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
233
+ @Mutation(returns => ScalarObject, { description: 'Approve one actuation command. The approver is the signed-in user.' })
234
+ async approveTwinCommand(
235
+ @Arg('id') id: string,
236
+ @Arg('note', { nullable: true }) note: string,
237
+ @Ctx() context: ResolverContext
238
+ ): Promise<any> {
239
+ const { domain, user } = context.state
240
+ const by = String(user?.email ?? user?.name ?? '').trim()
241
+ if (!by) throw new Error('๋ˆ„๊ฐ€ ์Šน์ธํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค โ€” ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค')
242
+
243
+ return approve(databaseCommandStore(context.state.tx), domain.id, id, {
244
+ by,
245
+ at: new Date().toISOString(),
246
+ ...(note?.trim() ? { note: note.trim() } : {})
247
+ })
248
+ }
249
+
250
+ /**
251
+ * ๊ฑฐ์ ˆํ•œ๋‹ค โ€” ์—ฌ๊ธฐ์„œ ๋๋‚œ๋‹ค.
252
+ *
253
+ * ์‚ฌ์œ ๋ฅผ ์š”๊ตฌํ•œ๋‹ค. ๊ฑฐ์ ˆ์€ ํ˜„์žฅ์ด ๊ธฐ๋‹ค๋ฆฌ๋˜ ์กฐ์น˜๋ฅผ ์ทจ์†Œํ•˜๋Š” ๊ฒƒ์ด๊ณ , ์ด์œ  ์—†์ด ๊ฑฐ์ ˆ๋˜๋ฉด ๋‚ธ ์ชฝ์ด ๋‹ค์‹œ
254
+ * ๋‚ธ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ๊ฐ™์€ ๊ฒƒ์ด ๋Œ€๊ธฐ ์ค„์— ๊ณ„์† ๋Œ์•„์˜จ๋‹ค.
255
+ */
256
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
257
+ @Mutation(returns => ScalarObject, { description: 'Reject one actuation command. A reason is required.' })
258
+ async rejectTwinCommand(
259
+ @Arg('id') id: string,
260
+ @Arg('reason') reason: string,
261
+ @Ctx() context: ResolverContext
262
+ ): Promise<any> {
263
+ const { domain, user } = context.state
264
+ const by = String(user?.email ?? user?.name ?? '').trim()
265
+ if (!by) throw new Error('๋ˆ„๊ฐ€ ๊ฑฐ์ ˆํ•˜๋Š”์ง€ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค โ€” ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค')
266
+ if (!reason?.trim()) throw new Error('๊ฑฐ์ ˆ ์‚ฌ์œ ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค โ€” ์ด์œ  ์—†์ด ๊ฑฐ์ ˆ๋˜๋ฉด ๊ฐ™์€ ์กฐ์น˜๊ฐ€ ๋‹ค์‹œ ์˜ฌ๋ผ์˜ต๋‹ˆ๋‹ค')
267
+
268
+ return reject(databaseCommandStore(context.state.tx), domain.id, id, { by, at: new Date().toISOString(), note: reason.trim() })
269
+ }
270
+
271
+ /**
272
+ * ์–ด๋Œ‘ํ„ฐ์— ๋„˜๊ธด๋‹ค โ€” **์Šน์ธ์„ ์ง€๋‚˜์ง€ ์•Š์€ ๊ฒƒ์€ ์—ฌ๊ธฐ์„œ ๋ง‰ํžŒ๋‹ค.**
273
+ *
274
+ * โ”€โ”€ ์ด๋ฆ„์ด ์™œ `dispatchTwinCommand` ๊ฐ€ ์•„๋‹Œ๊ฐ€ (2026-09-01) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
275
+ * ๊ทธ ์ด๋ฆ„์€ ์ด๋ฏธ **์ปค๋„ ์ž๊ทน**์ด ์“ด๋‹ค(`TwinControlMutation.dispatchTwinCommand` โ€”
276
+ * `order.hold`ยท`resource.repair` ๋ฅผ ์ปค๋„์— ๋„ฃ๋Š”๋‹ค). ๊ฐ™์€ ์ด๋ฆ„์„ ๋‘ ๋ฒˆ ์„ ์–ธํ•˜๋ฉด type-graphql ์ด
277
+ * ์˜ค๋ฅ˜ ์—†์ด ํ•˜๋‚˜๋งŒ ๋‚จ๊ธฐ๊ณ , ์‹ค์ œ๋กœ ์ด ๋ฎคํ…Œ์ด์…˜์ด ํ†ต์งธ๋กœ ๊ฐ€๋ ค์ ธ ์žˆ์—ˆ๋‹ค โ€” ๊ฑธ์–ด ๋ณด๊ณ  ์•Œ์•˜๋‹ค.
278
+ *
279
+ * ์ž๊ทน๊ณผ ์กฐ์น˜๊ฐ€ ๋‹ค๋ฅธ ์–ดํœ˜๋ผ๋Š” ๊ฒƒ์„ ๊ณ„์•ฝ์—์„œ ๊ฐˆ๋ผ ๋†“๊ณ (ยง`propose` ์˜ ๊ฑฐ์ ˆ), ์ •์ž‘ API ์ด๋ฆ„์—์„œ ๊ฒน์ณ
280
+ * ๋‘์—ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ์กฐ์น˜์ž„์„ ์ด๋ฆ„์— ์ ๋Š”๋‹ค.
281
+ *
282
+ * ํ™”๋ฉด์ด ์ด๊ฒƒ์„ ๋”ฐ๋กœ ๋ถ€๋ฅด๋Š” ์ด์œ ๋Š”, ์Šน์ธ๊ณผ ๋„˜๊น€์ด ๋‹ค๋ฅธ ์ˆœ๊ฐ„์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์Šน์ธํ•œ ์‚ฌ๋žŒ๊ณผ ๋„˜๊ธด ์‹œ๊ฐ์ด
283
+ * ๊ฐ™์•„์•ผ ํ•  ์ด์œ ๊ฐ€ ์—†๊ณ , ์–ด๋Œ‘ํ„ฐ๊ฐ€ ์ฃฝ์–ด ์žˆ์œผ๋ฉด ์Šน์ธ์€ ๋‚จ์•„ ์žˆ์–ด์•ผ ํ•œ๋‹ค.
284
+ *
285
+ * โ”€โ”€ ์—ฌ๊ธฐ๊ฐ€ ๋Š๊ฒจ ์žˆ์—ˆ๋‹ค (2026-09-02) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
286
+ * ์ด ์ž๋ฆฌ๊ฐ€ **์–ด๋Œ‘ํ„ฐ๋ฅผ ์ฐพ์ง€๋„ ์•Š๊ณ ** ใ€Œ๊ตฌํ˜„์ด ์—†์Šต๋‹ˆ๋‹คใ€๋ฅผ ๋‹ตํ–ˆ๋‹ค. MES ์ปค๋„ฅํ„ฐ๋Š” ๊ทธ๋•Œ ์ด๋ฏธ `actuate`
287
+ * ๋ฅผ ๋‹ค ๋งŒ๋“ค์–ด ๋‘์—ˆ๋‹ค. ๊ทธ ๋‹ต์„ ์‚ฌ์‹ค๋กœ ์ฝ์–ด์„œ ์„ค๊ณ„ ๋ฌธ์„œ์—๋„, ๋‹ค๋ฅธ ๋ ˆ์ธ์—๋„ ๊ทธ๋ ‡๊ฒŒ ์•Œ๋ ธ๋‹ค โ€” ๋‘˜ ๋‹ค
288
+ * ํ‹€๋ ธ๋‹ค.
289
+ *
290
+ * ์ด์ œ ์›๋ณธ์„ ์ฐพ์•„ ๊ทธ ์›๋ณธ์ด ์„ ์–ธํ•œ ๋Šฅ๋ ฅ์œผ๋กœ ํŒ๋‹จํ•œ๋‹ค(ยง`routeActuation`). ๋ณด๋‚ด์ง€ ๋ชปํ•˜๋ฉด **๋ฌด์—‡์ด
291
+ * ์—†์–ด์„œ์ธ์ง€** ๋งํ•œ๋‹ค. ์กฐ์šฉํžˆ ์„ฑ๊ณต์„ ๋‹ตํ•˜๋ฉด ํ™”๋ฉด์€ ๋‚˜๊ฐ„ ์ค„๋กœ ์ฝ๊ณ , ํ˜„์žฅ์€ ์˜ค์ง€ ์•Š๋Š” ์ง€์‹œ๋ฅผ ๊ธฐ๋‹ค๋ฆฐ๋‹ค.
292
+ *
293
+ * โ”€โ”€ ์—ฌ๊ธฐ์—๋Š” ํŠธ๋žœ์žญ์…˜์„ ๊ฑธ์ง€ ์•Š๋Š”๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
294
+ * ๋„˜๊ธฐ๊ธฐ ์ „์— ใ€Œ๋„˜๊ธฐ๋Š” ์ค‘ใ€์„ ๋จผ์ € ์ ๋Š” ๊ฒƒ์ด ์ด ํ•จ์ˆ˜์˜ ์š”์ ์ด๋‹ค. ํ•œ ํŠธ๋žœ์žญ์…˜์— ๋ฌถ์œผ๋ฉด ๊ทธ ๊ธฐ๋ก์ด
295
+ * ์ปค๋ฐ‹ ์ „๊นŒ์ง€ ์•„๋ฌด์—๊ฒŒ๋„ ๋ณด์ด์ง€ ์•Š๊ณ , ๋„˜๊ธฐ๋Š” ์‚ฌ์ด์— ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ฃฝ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์‚ฌ๋ผ์ง„๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ €์ชฝ์€
296
+ * ๋ฐ›์•˜๋Š”๋ฐ ์šฐ๋ฆฌ๋Š” ์•ˆ ๋ณด๋‚ธ ์ค„ ์•Œ๊ณ  ๋‹ค์‹œ ๋ณด๋‚ธ๋‹ค โ€” ํ˜„์žฅ์ด ๋‘ ๋ฒˆ ์›€์ง์ธ๋‹ค.
297
+ */
298
+ /**
299
+ * **์ด ์กฐ์น˜๋ฅผ ๋„˜๊ธธ ์ˆ˜ ์žˆ๋‚˜** โ€” ๋ˆ„๋ฅด๊ธฐ ์ „์— ๋‹ตํ•œ๋‹ค.
300
+ *
301
+ * โ”€โ”€ ์™œ ์ด ์ž๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ๊ฐ€ (2026-09-03) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
302
+ * ํŒ์ • ํ•จ์ˆ˜ ๋‘˜์„ ๋งŒ๋“ค์–ด ๋‘๊ณ  **๋ถ€๋ฅด๋Š” ์ž๋ฆฌ๋ฅผ ์•ˆ ๋งŒ๋“ค์—ˆ๋‹ค**(`actuationTargetOf`ยท
303
+ * `actuationReadinessOf`). ๊ทธ๋ž˜์„œ ํ™”๋ฉด์ด ๋ฌผ์„ ๋ฐฉ๋ฒ•์ด ์—†์—ˆ๊ณ , ๋ชป ๋„˜๊ธฐ๋Š” ์กฐ์น˜๋„ ๋ˆŒ๋Ÿฌ ๋ณด๊ณ  ์•Œ๊ฒŒ ๋๋‹ค.
304
+ * ์ธํ‹ฐ๊ทธ๋ ˆ์ด์…˜ ๋ ˆ์ธ์ด ๊ทธ ์‚ฌ์‹ค์„ ๋‘ ๋ฒˆ ์งš์–ด ์คฌ๋‹ค โ€” ์ž๋ฆฌ๋Š” ์žˆ๊ณ  ๊ธธ์ด ์—†๋Š” ๊ทธ ๋ถ€๋ฅ˜๋‹ค.
305
+ *
306
+ * ๊ทธ๋™์•ˆ ๊ฐ’์ด ์‹ค์ œ๋กœ ์—†์–ด์กŒ๋‹ค. ์ €์ชฝ์— ๋น„๋ฐ€๊ฐ’์ด ์—†๋˜ ๋‚ , ์‚ฌ๋žŒ์ด ์Šน์ธ ํ™”๋ฉด๊นŒ์ง€ ๊ฐ€์„œ ๋ˆ„๋ฅด๊ณ  ์‹คํŒจ๋ฅผ
307
+ * ๋ณด๊ณ  ๋‹ค์‹œ ๋กœ๊ทธ์ธํ–ˆ๋‹ค. **์Šน์ธ์€ ์‚ด์•„ ์žˆ์ง€๋งŒ ์‚ฌ๋žŒ์˜ ์‹œ๊ฐ„์€ ๋Œ์•„์˜ค์ง€ ์•Š๋Š”๋‹ค.**
308
+ *
309
+ * โ”€โ”€ ๋‘˜์„ ํ•จ๊ป˜ ๋‹ตํ•œ๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
310
+ * ํ™”๋ฉด์ด ๋ฌป๋Š” ๊ฒƒ์€ ํ•˜๋‚˜๋‹ค โ€” ใ€Œ์ง€๊ธˆ ๋„˜๊ฒจ๋„ ๋˜๋‚˜ใ€. ๊ทธ ๋‹ต์ด ๋‘ ๊ฒน์ด๋ผ ๋‘˜์„ ํ•จ๊ป˜ ์‹ฃ๋Š”๋‹ค.
311
+ *
312
+ * target ๋ณด๋‚ผ ๋Œ€์ƒ์ด ์ •ํ•ด์ง€๋‚˜ โ€” ๋ง์„ ํƒ€์ง€ ์•Š๋Š”๋‹ค(์›๋ณธยท๋Šฅ๋ ฅยท์‚ฌ์ดํŠธ)
313
+ * readiness ์ €์ชฝ์ด ๋ฐ›์„ ์ค€๋น„๊ฐ€ ๋๋‚˜ โ€” ์ปค๋„ฅํ„ฐ๊ฐ€ ๋ง์„ ํƒˆ ์ˆ˜ ์žˆ๋‹ค
314
+ *
315
+ * **๋ฌธ์ด ์•„๋‹ˆ๋‹ค.** ์ด ๋‹ต์œผ๋กœ ๋„˜๊น€์„ ๋ง‰์ง€ ์•Š๋Š”๋‹ค(ยง`actuationReadinessOf`) โ€” ํ™”๋ฉด์ด ๋ฏธ๋ฆฌ ๋งํ•˜๋Š”
316
+ * ๋ฐ๊นŒ์ง€๋‹ค. ๊ทธ๋ž˜์„œ ์กฐํšŒ์ด๊ณ  ๋ฎคํ…Œ์ด์…˜์ด ์•„๋‹ˆ๋‹ค.
317
+ *
318
+ * ํ™”๋ฉด์ด **๋ชฉ๋ก์„ ๊ทธ๋ฆด ๋•Œ ๋ถ€๋ฅด์ง€ ๋ง ๊ฒƒ.** ์ปค๋„ฅํ„ฐ๊ฐ€ ์ €์ชฝ์— ๋ฌผ์„ ์ˆ˜ ์žˆ์–ด์„œ ๋ชฉ๋ก ํ•œ ์ค„๋งˆ๋‹ค ๋ง์„ ํƒ„๋‹ค.
319
+ * ์‚ฌ๋žŒ์ด ใ€Œ๋„˜๊ธฐ๊ธฐใ€๋ฅผ ์—ด ๋•Œ ํ•œ ๋ฒˆ์ด๋‹ค.
320
+ */
321
+ /*
322
+ * ๊ถŒํ•œ ์ง€์‹œ์ž๋ฅผ ๋‹ฌ์ง€ ์•Š๋Š”๋‹ค โ€” ์ด ํŒŒ์ผ์˜ ๋‹ค๋ฅธ ์กฐํšŒ ๋‘˜๊ณผ ๊ฐ™๋‹ค. **์“ฐ๊ธฐ๋งŒ ๊ฒŒ์ดํŠธ๋ฅผ ์ง€๋‚œ๋‹ค**(ADR-0027),
323
+ * ๊ทธ๋ฆฌ๊ณ  ์กฐํšŒ๋Š” ํ…Œ๋„ŒํŠธ๋กœ ์ขํ˜€์ง„๋‹ค. ์ฒ˜์Œ์— `privilege: "query"` ๋ฅผ ๋‹ฌ์•˜๋”๋‹ˆ ๊ถŒํ•œ ๋ฌธ์žฅ ๊ฒ€์‚ฌ๊ฐ€
324
+ * ใ€Œ์ •๋ณธ๊ณผ ๋‹ค๋ฅด๋‹คใ€๋กœ ์žก์•˜๋‹ค.
325
+ */
326
+ @Query(returns => ScalarObject, {
327
+ nullable: true,
328
+ description: 'Whether one approved actuation can be dispatched now โ€” target and far-side readiness. Advisory, not a gate.'
329
+ })
330
+ async twinActuationReadiness(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
331
+ const domainId = context.state.domain?.id
332
+ if (!domainId) throw new Error('ํ…Œ๋„ŒํŠธ๋ฅผ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค')
333
+
334
+ const command = await databaseCommandStore().load(domainId, id)
335
+ if (!command) throw new Error(`๋ชจ๋ฅด๋Š” ์กฐ์น˜์ž…๋‹ˆ๋‹ค (${id})`)
336
+
337
+ /*
338
+ * ์ƒํƒœ๋ฅผ ํ•จ๊ป˜ ๋‚ธ๋‹ค โ€” **์ด ์กฐ์น˜๊ฐ€ ๋„˜๊ธธ ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ์ธ๊ฐ€**๋Š” ์ €์ชฝ ์‚ฌ์ •๊ณผ ๋‹ค๋ฅธ ๋ฌผ์Œ์ด๋‹ค. ์ด๋ฏธ
339
+ * `acked` ์ธ ๊ฒƒ์€ ์ €์ชฝ์ด ์•„๋ฌด๋ฆฌ ์ค€๋น„๋ผ ์žˆ์–ด๋„ ๋‹ค์‹œ ๋„˜๊ธฐ์ง€ ์•Š๋Š”๋‹ค(์ข…์ฐฉ).
340
+ */
341
+ const dispatchable = nextCommandState(command.state, 'dispatch') !== undefined
342
+
343
+ const [target, readiness] = await Promise.all([
344
+ actuationTargetOf(domainId, command.instanceId),
345
+ actuationReadinessOf(domainId, command.instanceId)
346
+ ])
347
+
348
+ return { id, state: command.state, dispatchable, target, readiness }
349
+ }
350
+
351
+ /*
352
+ * ๊ถŒํ•œ ์ง€์‹œ์ž๋ฅผ ๋‹ฌ์ง€ ์•Š๋Š”๋‹ค โ€” ์ด ํŒŒ์ผ์˜ ๋‹ค๋ฅธ ์กฐํšŒ๋“ค๊ณผ ๊ฐ™๋‹ค(ADR-0027: ์“ฐ๊ธฐ๋งŒ ๊ฒŒ์ดํŠธ๋ฅผ ์ง€๋‚œ๋‹ค).
353
+ */
354
+ @Query(returns => ScalarObject, {
355
+ nullable: true,
356
+ description: 'What happened to one actuation โ€” for the person who just approved it. Includes whether work remains at the source.'
357
+ })
358
+ async twinActuationOutcome(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
359
+ const domainId = context.state.domain?.id
360
+ if (!domainId) throw new Error('ํ…Œ๋„ŒํŠธ๋ฅผ ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค')
361
+
362
+ const command = await databaseCommandStore().load(domainId, id)
363
+ if (!command) throw new Error(`๋ชจ๋ฅด๋Š” ์กฐ์น˜์ž…๋‹ˆ๋‹ค (${id})`)
364
+
365
+ /*
366
+ * **์Šน์ธ ์งํ›„์— ๋ถ€๋ฅด๋Š” ์ž๋ฆฌ๋‹ค.** ๋ฐœ์‹ ์€ ์Šน์ธ ์‘๋‹ต์ด ๋‚˜๊ฐ„ ๋‹ค์Œ ํ‹ฑ์— ์ผ์–ด๋‚˜๋ฏ€๋กœ(ยง
367
+ * `dispatchAfterApproval`), ๊ทธ ์ˆœ๊ฐ„์—๋Š” ์•„์ง `approved` ์ผ ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๊ฒƒ์„ ๊ฐ์ถ”์ง€ ์•Š๋Š”๋‹ค โ€”
368
+ * ํ™”๋ฉด์ด ใ€Œ๋ณด๋‚ด๋Š” ์ค‘ใ€์„ ๊ทธ๋ฆฌ๊ณ  ์งง๊ฒŒ ๋‹ค์‹œ ๋ฌป๋Š”๋‹ค(UX ๋ ˆ์ธ ๊ทœ์น™: ๋Œ€๋žต 0.5์ดˆยท2์ดˆ ๋‘ ๋ฒˆ, ๊ทธ๋ž˜๋„
369
+ * ์•ˆ ๋๋‚˜๋ฉด ์กฐ์น˜ ๋ชฉ๋ก์œผ๋กœ ์•ˆ๋‚ด).
370
+ *
371
+ * ์—ฌ๊ธฐ์„œ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๋Š” ์ด์œ : ๊ธฐ๋‹ค๋ฆฌ๋ฉด ์ด ์กฐํšŒ๊ฐ€ ์ €์ชฝ ์™•๋ณต๋งŒํผ ๋ฉˆ์ถ”๊ณ , ์‚ฌ๋žŒ์ด ํ™”๋ฉด์„ ๋– ๋‚œ
372
+ * ๋’ค์—๋„ ๋ถ™์žก๋Š”๋‹ค.
373
+ */
374
+ return actuationOutcomeOf(command, nextCommandState(command.state, 'dispatch') !== undefined)
375
+ }
376
+
377
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
378
+ @Mutation(returns => ScalarObject, { description: 'Send one approved actuation command to its connector. Not a kernel stimulus.' })
379
+ async dispatchTwinActuation(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {
380
+ const { domain } = context.state
381
+
382
+ /* ํŠธ๋žœ์žญ์…˜์„ ๋„˜๊ธฐ์ง€ ์•Š๋Š”๋‹ค โ€” ์œ„์˜ ์ด์œ ๋‹ค. ์ง€์‹œ์ž๊ฐ€ ๋’ค์— ๋ถ™์–ด๋„ ์ด ์ž๋ฆฌ๋Š” ๋ฐ”๋€Œ์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค. */
383
+ return dispatch(databaseCommandStore(), domain.id, id, command =>
384
+ routeActuation(domain.id, command.instanceId, command)
385
+ )
386
+ }
387
+ }
@@ -0,0 +1,232 @@
1
+ /*
2
+ * **ํ˜„์žฅ์— ๋‚ด๋ฆฌ๋Š” ์กฐ์น˜ ํ•˜๋‚˜** โ€” ์ œ์•ˆ ยท ์Šน์ธ ยท ๋„˜๊น€ ยท ์‘๋‹ต์ด ํ•œ ํ–‰์— ๋‚จ๋Š”๋‹ค.
3
+ *
4
+ * โ”€โ”€ ์™œ ํ‘œ์ธ๊ฐ€ (2026-08-31) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * ์Šน์ธ์ด ํ”„๋กœ์„ธ์Šค ์•ˆ์—๋งŒ ์žˆ์œผ๋ฉด ํŠธ์œˆ์ด ์žฌ๊ธฐ๋™ํ•œ ์ˆœ๊ฐ„ ์Šน์ธ ๋Œ€๊ธฐ ์ค„์ด ์‚ฌ๋ผ์ง„๋‹ค. ๊ทธ๋•Œ ์‚ฌ๋žŒ์€ **์ž๊ธฐ๊ฐ€
6
+ * ์Šน์ธํ•œ ๊ฒƒ์ด ์‹คํ–‰๋๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค.** ์กฐ์น˜๋Š” ๋˜๋Œ๋ฆฌ๊ธฐ ์–ด๋ ค์šด ๊ฒƒ์ด๋ฏ€๋กœ ๊ทธ ๋ชจ๋ฆ„์ด ๊ทธ๋Œ€๋กœ ์‚ฌ๊ณ ๊ฐ€ ๋œ๋‹ค.
7
+ *
8
+ * ๊ทธ๋ฆฌ๊ณ  ์ด ํ–‰์€ **๋ˆ„๊ฐ€ ์–ธ์ œ ์Šน์ธํ–ˆ๋‚˜**์˜ ๊ธฐ๋ก์ด๊ธฐ๋„ ํ•˜๋‹ค. ์‚ฌ๊ณ  ๋’ค์— ๊ทธ๊ฒƒ์„ ๋ฌผ์„ ์ž๋ฆฌ๊ฐ€ ์—ฌ๊ธฐ๋‹ค.
9
+ *
10
+ * โ”€โ”€ ํŒ์ •์€ ์—ฌ๊ธฐ ์—†๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
11
+ * ์–ด๋А ์ƒํƒœ์—์„œ ์–ด๋А ์‚ฌ๊ฑด์ด ํ—ˆ์šฉ๋˜๋Š”์ง€๋Š” ๊ณ„์•ฝ์ด ๋“ ๋‹ค(`@operato/ops-contract` ยง`nextCommandState`).
12
+ * ์ด ํŒŒ์ผ์€ ๊ทธ ๊ฐ’์„ ๋‹ด๊ธฐ๋งŒ ํ•œ๋‹ค โ€” ํŒ์ •์ด ๋‘ ๋ฒŒ์ด ๋˜๋ฉด ํ™”๋ฉด๊ณผ ์„œ๋ฒ„๊ฐ€ ๋‹ค๋ฅธ ๋‹ต์„ ๋‚ธ๋‹ค.
13
+ */
14
+ import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'
15
+ import { Field, ID, Int, ObjectType } from 'type-graphql'
16
+
17
+ import { Domain, ScalarObject } from '@things-factory/shell'
18
+
19
+ @Entity()
20
+ /* ๋Œ€๊ธฐ ์ค„์„ ์—ฌ๋Š” ์งˆ์˜๊ฐ€ ์ด ์„ธ ์นธ์œผ๋กœ ๋ฌป๋Š”๋‹ค โ€” ์–ด๋А ํ…Œ๋„ŒํŠธ์˜ ยท ์–ด๋А ํŠธ์œˆ์˜ ยท ์–ด๋А ์ƒํƒœ์ธ๊ฐ€. */
21
+ @Index('ix_twin_command_0', (e: TwinCommandRecord) => [e.domain, e.instanceId, e.state])
22
+ @ObjectType({ description: 'One actuation command proposed against a twin, with its approval trail.' })
23
+ export class TwinCommandRecord {
24
+ @PrimaryGeneratedColumn('uuid')
25
+ @Field(type => ID, { description: 'Unique identifier of the command.' })
26
+ readonly id: string
27
+
28
+ @ManyToOne(type => Domain)
29
+ @Field(type => Domain, { nullable: true, description: 'Tenant this command belongs to.' })
30
+ domain?: Domain
31
+
32
+ @RelationId((e: TwinCommandRecord) => e.domain)
33
+ domainId?: string
34
+
35
+ @Column()
36
+ @Field({ description: 'Twin instance this command acts on.' })
37
+ instanceId: string
38
+
39
+ @Column()
40
+ @Field({ description: 'Command type in twin vocabulary. Translating it into a legacy call is the adapter job.' })
41
+ type: string
42
+
43
+ @Column({ type: 'simple-json', nullable: true })
44
+ @Field(type => ScalarObject, { nullable: true, description: 'Command payload in twin vocabulary.' })
45
+ payload?: any
46
+
47
+ @Column()
48
+ @Field({ description: 'Who proposed it: operator, rule, ai or schedule. A person weighs this when approving.' })
49
+ origin: string
50
+
51
+ /**
52
+ * **๋‚ธ ์‚ฌ๋žŒ** โ€” ์ข…๋ฅ˜๊ฐ€ ์•„๋‹ˆ๋ผ ์‚ฌ๋žŒ์ด๋‹ค.
53
+ *
54
+ * `origin` ์€ ใ€Œ์‚ฌ๋žŒ์ด ๋ƒˆ๋‚˜ ยท ๊ทœ์น™์ด ๋ƒˆ๋‚˜ใ€๋งŒ ๋งํ•œ๋‹ค. ๊ทธ๊ฒƒ๋งŒ์œผ๋กœ๋Š” **๋‚ธ ์‚ฌ๋žŒ์ด ์ž๊ธฐ ๊ฒƒ์„ ์Šน์ธํ•˜๋Š” ๊ฒƒ**์„
55
+ * ๋ง‰์„ ์ˆ˜ ์—†๊ณ , ๊ทธ๋Ÿฌ๋ฉด ๊ฒŒ์ดํŠธ๊ฐ€ ์‹œ๋Љ์ด ๋œ๋‹ค. ๋˜๋Œ๋ฆด ์ˆ˜ ์—†๋Š” ์กฐ์น˜์— ๋‘ ์‚ฌ๋žŒ์ด ํ•„์š”ํ•˜๋‹ค๋Š” ๊ฒƒ์ด ์ด ํ‘œ์˜
56
+ * ์กด์žฌ ์ด์œ ๋‹ค.
57
+ *
58
+ * ๊ทœ์น™ยทAI ๊ฐ€ ๋‚ธ ๊ฒƒ์€ ๋น„์–ด ์žˆ๋‹ค โ€” ์‚ฌ๋žŒ์ด ์•„๋‹ˆ๋‹ค.
59
+ */
60
+ @Column({ nullable: true })
61
+ @Field({ nullable: true, description: 'The person who proposed it. Absent when a rule or model proposed it.' })
62
+ proposedBy?: string
63
+
64
+ /**
65
+ * **๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ๋Š”๊ฐ€** โ€” ๊ฒŒ์ดํŠธ๊ฐ€ ๊ฑธ๋ฆฌ๋Š” ๊ธฐ์ค€์ด๋‹ค.
66
+ *
67
+ * ๋น„์–ด ์žˆ์œผ๋ฉด ใ€Œ๋˜๋Œ๋ฆด ์ˆ˜ ์—†๋‹คใ€๋กœ ์ฝ๋Š”๋‹ค(ยง`requiresApproval`). ๋ชจ๋ฆ„์„ ๋˜๋Œ๋ฆด ์ˆ˜ ์žˆ๋‹ค๋กœ ์ฝ์œผ๋ฉด ๊ทธ
68
+ * ์ปค๋งจ๋“œ๊ฐ€ ์Šน์ธ ์—†์ด ๋‚˜๊ฐ„๋‹ค.
69
+ */
70
+ @Column({ nullable: true })
71
+ @Field({ nullable: true, description: 'Whether the effect can be undone. Absent means it cannot, and a person must approve.' })
72
+ reversible?: boolean
73
+
74
+ @Column()
75
+ @Field({ description: 'proposed ยท approved ยท rejected ยท dispatched ยท acked ยท failed.' })
76
+ state: string
77
+
78
+ /**
79
+ * **๋ฌด์—‡ ๋•Œ๋ฌธ์— ๋ƒˆ๋‚˜** โ€” ๊ทœ์น™์ด ๋‚ธ ์กฐ์น˜๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ด€์ธก์˜ ์‹๋ณ„์ž(`Attention.id`).
80
+ *
81
+ * ๋‘ ๊ฐ€์ง€๋ฅผ ํ•œ๋‹ค.
82
+ *
83
+ * ๊ฐ™์€ ์กฐ๊ฑด์„ ๋‘ ๋ฒˆ ์˜ฌ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค ๊ฒฐ์ •์„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์ปค๋งจ๋“œ์˜ ์ด ๊ฐ’์„ ๋ชจ์•„ ํŒ์ •์— ๋„˜๊ธด๋‹ค
84
+ * ์Šน์ธํ•˜๋Š” ์‚ฌ๋žŒ์ด ๊ทผ๊ฑฐ๋ฅผ ๋ณธ๋‹ค ใ€Œ๋ฌด์Šจ ์ผ์ด ์žˆ์–ด์„œ ์ด๊ฒƒ์ด ์˜ฌ๋ผ์™”๋‚˜ใ€
85
+ *
86
+ * ์•ž์—ฃ๊ฒƒ์ด ์ด ์นธ์˜ ์กด์žฌ ์ด์œ ๋‹ค. ํ”„๋กœ์„ธ์Šค ๊ธฐ์–ต์œผ๋กœ ์ค‘๋ณต์„ ๋ง‰์œผ๋ฉด **์žฌ๊ธฐ๋™์ด ๊ณง ์ค‘๋ณต**์ด ๋œ๋‹ค โ€” ๋‹ค์‹œ ์„ 
87
+ * ํŠธ์œˆ์ด ๊ฐ™์€ ์กฐ๊ฑด์„ ์ฒ˜์Œ ๋ณด๋Š” ๊ฒƒ์œผ๋กœ ์ฝ๊ณ  ๋Œ€๊ธฐ ์ค„์— ๊ฐ™์€ ์ง€์‹œ๋ฅผ ๋˜ ์˜ฌ๋ฆฐ๋‹ค.
88
+ *
89
+ * ์‚ฌ๋žŒ์ด ๋‚ธ ๊ฒƒ์€ ๋น„์–ด ์žˆ๋‹ค. ์‚ฌ๋žŒ์€ ์ž๊ธฐ๊ฐ€ ์™œ ๋‚ด๋Š”์ง€ ์•Œ๊ณ  ์žˆ๊ณ , ๊ทธ ์ด์œ ๋ฅผ ๊ฐ’์œผ๋กœ ์š”๊ตฌํ•˜๋ฉด ์•„๋ฌด ๊ธ€์ž๋‚˜ ์ ํžŒ๋‹ค.
90
+ */
91
+ @Column({ nullable: true })
92
+ @Field({ nullable: true, description: 'The observation that caused a rule to propose this. Empty when a person proposed it.' })
93
+ causeRef?: string
94
+
95
+ /** ์–ด๋А ๊ทœ์น™์ด ๋ƒˆ๋‚˜ โ€” ๊ทœ์น™์„ ๋„๊ฑฐ๋‚˜ ๊ณ ์น  ๋•Œ ๊ทธ ๊ทœ์น™์ด ๋ฌด์—‡์„ ๋ƒˆ๋Š”์ง€ ๋˜์งš์„ ์ž๋ฆฌ. */
96
+ @Column({ nullable: true })
97
+ @Field({ nullable: true, description: 'The rule that proposed this, when a rule did.' })
98
+ ruleRef?: string
99
+
100
+ /**
101
+ * **๊ฒฐ์žฌ์„ ์— ์˜ฌ๋ผ๊ฐ„ ์ž๋ฆฌ** โ€” ์—…๋ฌด ๋ชฉ๋ก(`@things-factory/worklist`)์˜ ์•กํ‹ฐ๋น„ํ‹ฐ ์ธ์Šคํ„ด์Šค ์‹๋ณ„์ž.
102
+ *
103
+ * ์ด ๊ฐ’์ด ์žˆ์œผ๋ฉด ์Šน์ธยท๊ฑฐ์ ˆ์€ **๊ทธ ๋ชฉ๋ก์—์„œ๋งŒ** ํ•œ๋‹ค. ์—ฌ๊ธฐ ์žˆ๋Š” ์ง์ ‘ ๊ฒฐ์ • ๋ฎคํ…Œ์ด์…˜์€ ๋ง‰ํžŒ๋‹ค โ€” ๋ฌธ์ด ๋‘
104
+ * ๋ฒŒ์ด๋ฉด ๊ฒฐ์žฌ์„ ์—๋Š” ๋Œ€๊ธฐ๋กœ ๋‚จ์•„ ์žˆ๋Š”๋ฐ ์กฐ์น˜๋Š” ์ด๋ฏธ ๋‚˜๊ฐ„ ์ƒํƒœ๊ฐ€ ๋งŒ๋“ค์–ด์ง„๋‹ค.
105
+ *
106
+ * ๋น„์–ด ์žˆ์œผ๋ฉด ๊ฒฐ์žฌ์„ ์„ ์“ฐ์ง€ ์•Š๋Š” ๋ฐฐํฌ๋‹ค. ๊ทธ๋•Œ๋Š” ์ง์ ‘ ๊ฒฐ์ •์ด ์œ ์ผํ•œ ๋ฌธ์ด๋‹ค.
107
+ */
108
+ @Column({ nullable: true })
109
+ @Field({ nullable: true, description: 'Approval record this command waits on. While set, decisions come only from there.' })
110
+ approvalRef?: string
111
+
112
+ /* โ”€โ”€ ์Šน์ธ์˜ ๊ธฐ๋ก โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
113
+ * ํ•œ ์นธ์— JSON ์œผ๋กœ ๋‹ด์ง€ ์•Š๊ณ  ์„ธ ์นธ์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค โ€” ใ€Œ๋ˆ„๊ฐ€ ์Šน์ธํ–ˆ๋‚˜ใ€๋กœ ๋ฌป๋Š” ์งˆ์˜๊ฐ€ ์‹ค์ œ๋กœ ์˜จ๋‹ค.
114
+ *
115
+ * ๊ฒฐ์žฌ์„ ์„ ์ง€๋‚œ ๊ฒฝ์šฐ์—๋„ ์—ฌ๊ธฐ์— ์ ๋Š”๋‹ค. ํŒ์ •์˜ ์˜จ์ „ํ•œ ๊ธฐ๋ก(์œ„์ž„ยท์ƒ์‹ ยทํšŒ์ฐจ)์€ ๊ฒฐ์žฌ์„ ์ด ๋“ค๊ณ , ์ด ์„ธ ์นธ์€
116
+ * **์ด ์ปค๋งจ๋“œ์— ๋Œ€ํ•œ ๋งˆ์ง€๋ง‰ ํŒ์ •**์ด๋‹ค โ€” ์ปค๋งจ๋“œ ๋ชฉ๋ก์ด ๊ฒฐ์žฌ์„ ์„ ์กฐํšŒํ•˜์ง€ ์•Š๊ณ ๋„ ใ€Œ๋ˆ„๊ฐ€ ์–ธ์ œใ€๋ฅผ ๋ณด์—ฌ์•ผ ํ•œ๋‹ค.
117
+ */
118
+ @Column({ nullable: true })
119
+ @Field({ nullable: true, description: 'Who approved or rejected it.' })
120
+ decidedBy?: string
121
+
122
+ /*
123
+ * ISO ๋ฌธ์ž์—ด๋กœ ๋“ ๋‹ค โ€” `datetime` ์€ ๋“œ๋ผ์ด๋ฒ„ ๋ฐฉ์–ธ์ด๋ผ ๋‹ค์„ฏ ๋“œ๋ผ์ด๋ฒ„๋ฅผ ๊ทธ๋Œ€๋กœ ์ง€๋‚˜์ง€ ๋ชปํ•˜๊ณ ,
124
+ * sqlite ๋Š” ๊ทธ๊ฒƒ์„ ๊ฒ€์ฆํ•˜์ง€ ์•Š์•„ ๊ฐœ๋ฐœ์—์„œ ์žกํžˆ์ง€ ์•Š๋Š”๋‹ค. ์ด ์ €์žฅ์†Œ์˜ ๋‹ค๋ฅธ ์‹œ๊ฐ ์นธ๋„ ๊ทธ ๋ชจ์–‘์ด๋‹ค
125
+ * (ยง`TwinReference.lastSyncedAt`). ๊ณ„์•ฝ์˜ `CommandApproval.at` ๋„ ISO ๋ฌธ์ž์—ด์ด๋ผ ์˜ฎ๊ธธ ๊ฒƒ์ด ์—†๋‹ค.
126
+ */
127
+ @Column({ nullable: true })
128
+ @Field({ nullable: true, description: 'When it was approved or rejected (ISO 8601).' })
129
+ decidedAt?: string
130
+
131
+ @Column({ nullable: true })
132
+ @Field({ nullable: true, description: 'Why โ€” free text the approver wrote.' })
133
+ decisionNote?: string
134
+
135
+ /**
136
+ * ์–ด๋Œ‘ํ„ฐ๊ฐ€ ๋Œ๋ ค์ค€ ๊ฒƒ โ€” ์ €์ชฝ์—์„œ ๋ฌด์—‡์ด ๋˜์—ˆ๋Š”์ง€ ๋˜์งš์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.
137
+ *
138
+ * ์ด๊ฒƒ์ด ์—†์œผ๋ฉด ๋˜๋Œ๋ฆด ๋•Œ **๋ฌด์—‡์„ ๋˜๋Œ๋ฆด์ง€ ๋งํ•  ์ˆ˜ ์—†๋‹ค.**
139
+ */
140
+ @Column({ nullable: true })
141
+ @Field({ nullable: true, description: 'Reference the adapter returned. Without it a reversal cannot name what to undo.' })
142
+ dispatchRef?: string
143
+
144
+ /*
145
+ * โ”€โ”€ ์ €์ชฝ์—์„œ ์‹ค์ œ๋กœ ๋ฌด์—‡์ด ๋˜์—ˆ๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
146
+ * ์ƒํƒœ(`state`)์™€ ๋‹ค๋ฅธ ์ถ•์ด๋‹ค. `acked` ๋Š” ใ€Œ์ €์ชฝ์ด ๋‹ตํ–ˆ๋‹คใ€๋กœ ๋๋‚˜๊ณ , ์ด ์…‹์€ ๊ทธ ๋’ค์— ๊ด€์ธก์œผ๋กœ
147
+ * ๋Œ์•„์˜จ ์‚ฌ์‹ค์ด๋‹ค. ๋˜๋Œ์•„์˜ค๋Š” ๊ด€์ธก์ด ์—†๋Š” ์ปค๋„ฅํ„ฐ์˜ ์กฐ์น˜๋Š” ์ด ์…‹์ด ์˜์›ํžˆ ๋นˆ๋‹ค โ€” ๊ทธ๊ฒƒ์ด ์ •์ƒ์ด๊ณ ,
148
+ * ๋นˆ ๊ฒƒ์€ ใ€Œํšจ๊ณผ๊ฐ€ ์—†์—ˆ๋‹คใ€๊ฐ€ ์•„๋‹ˆ๋ผ **ใ€Œ๋ชจ๋ฅธ๋‹คใ€**๋‹ค.
149
+ */
150
+
151
+ /**
152
+ * ๋ฐ›์•„๋“ค์˜€์œผ๋‚˜ ์ €์ชฝ์ด ๋ง๋ถ™์ธ ๋ง โ€” **์‹คํŒจ๊ฐ€ ์•„๋‹ˆ๊ณ  ใ€Œ๋๋‚ฌ๋‹คใ€๋„ ์•„๋‹ˆ๋‹ค.**
153
+ *
154
+ * MES ๊ฐ€ ์ง€์‹œ๋ฅผ ์ ‘์ˆ˜ํ•ด ๋†“๊ณ  ์ง€์‹œ์„œ๋ฅผ ๋ชป ๋ถ™์ด๋ฉด ๊ทธ ์‚ฌ์‹ค์ด ์—ฌ๊ธฐ ์˜จ๋‹ค. `error` ์™€ ๋‹ค๋ฅธ ์นธ์ด์–ด์•ผ ํ•œ๋‹ค โ€”
155
+ * `error` ์— ์ ์œผ๋ฉด ์‹คํŒจ๋กœ ์ฝํ˜€ ์ปค๋งจ๋“œ๊ฐ€ `failed` ๋กœ ์•‰๋Š”๋‹ค.
156
+ */
157
+ @Column({ nullable: true })
158
+ @Field({ nullable: true, description: 'What the source added while accepting โ€” not a failure, not a completion. Kept verbatim.' })
159
+ dispatchNote?: string
160
+
161
+ /** ์ €์ชฝ์ด ๋ณด๊ณ ํ•œ ์ƒํƒœ โ€” **ํ•ด์„ํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ.** ์–ด๋А ๊ฐ’์ด ใ€Œ๋‚˜๊ฐ”๋‹คใ€์ธ์ง€๋Š” ํ˜„์žฅ์ด ์•ˆ๋‹ค. */
162
+ @Column({ nullable: true })
163
+ @Field({ nullable: true, description: 'Status the source reported for this instruction, kept verbatim. The kernel does not interpret it.' })
164
+ effectStatus?: string
165
+
166
+ /** ๊ทธ ๋ณด๊ณ ๋ฅผ ๊ด€์ธกํ•œ ์‹œ๊ฐ. ์ปฌ๋Ÿผ ํƒ€์ž…์„ ์ ์ง€ ์•Š๋Š”๋‹ค โ€” ๋“œ๋ผ์ด๋ฒ„ ๋ฐฉ์–ธ์ด๋ผ ๊ฐœ๋ฐœ์—์„œ ์•ˆ ์žกํžŒ๋‹ค. */
167
+ @Column({ nullable: true })
168
+ @Field({ nullable: true, description: 'When that report was observed. Empty means no report has been correlated yet.' })
169
+ effectAt?: string
170
+
171
+ /** ๋ช‡ ๋ฒˆ ๋ณด๊ณ ๋๋‚˜ โ€” ํ•œ ๋ฒˆ๋„ ์•ˆ ๋ฐ”๋€ ๊ฒƒ๊ณผ ๋‹ค์„ฏ ๋ฒˆ ๋ฐ”๋€ ๊ฒƒ์€ ๋‹ค๋ฅธ ์ผ์ด๋‹ค. */
172
+ @Column({ type: 'int', nullable: true })
173
+ @Field(type => Int, { nullable: true, description: 'How many reports have been correlated to this instruction.' })
174
+ effectSeen?: number
175
+
176
+ /**
177
+ * ์ €์ชฝ์ด ๋ฐ›์œผ๋ฉด์„œ ๋ง๋ถ™์ธ ๋ง ์ค‘ **๋‹ค์Œ์— ํ•  ์ผ** ํ•œ ์ค„ โ€” ์›์ธ์€ `dispatchNote` ๋‹ค.
178
+ *
179
+ * ์™œ ๋‘˜๋กœ ๋‚˜๋ˆ„๋‚˜: ์ฝ๋Š” ์‚ฌ๋žŒ์€ ใ€Œ๊ทธ๋ž˜์„œ ๋‚ด๊ฐ€ ๋ญ˜ ํ•ด์•ผ ํ•˜๋‚˜ใ€๋ฅผ ๋จผ์ € ์ฐพ๋Š”๋‹ค(UX ๋ ˆ์ธ). ํ•œ ๋ฌธ์žฅ์œผ๋กœ
180
+ * ์ฃผ๋ฉด ํ™”๋ฉด์ด ์ž๋ฅด๊ฒŒ ๋˜๊ณ , ์ž๋ฅด๋Š” ๊ทœ์น™์ด ํ™”๋ฉด๋งˆ๋‹ค ์ƒ๊ธด๋‹ค. ์–ด๋Œ‘ํ„ฐ๊ฐ€ ์ด๋ฏธ ๋‘˜๋กœ ์•Œ๊ณ  ์žˆ์œผ๋‹ˆ
181
+ * ๋ถ™์ด์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ๋ฐ›๋Š”๋‹ค.
182
+ */
183
+ @Column({ nullable: true })
184
+ @Field({ nullable: true, description: 'The next action the source expects, as one line. The cause is dispatchNote.' })
185
+ dispatchNoteNext?: string
186
+
187
+ /**
188
+ * **๋‹ค์‹œ ํ•ด์„œ ๋  ์ผ์ธ๊ฐ€** โ€” ์–ด๋Œ‘ํ„ฐ๊ฐ€ ์‹คํŒจ๋งˆ๋‹ค ๋‹ตํ•œ๋‹ค(ยง`CommandAck.retry`).
189
+ *
190
+ * again ๊ทธ๋Œ€๋กœ ๋‹ค์‹œ ํ•ด ๋ณผ ๋งŒํ•˜๋‹ค โ€” ๋ชป ๋‹ฟ์•˜๊ฑฐ๋‚˜ ์ €์ชฝ์ด ์ž ๊น ํ”๋“ค๋ ธ๋‹ค
191
+ * after-fix ์‚ฌ๋žŒ์ด ๊ณ ์นœ ๋’ค ๊ทธ๋Œ€๋กœ ๋‚˜๊ฐ„๋‹ค โ€” ์„ค์ • ๋ฌธ์ œ
192
+ * never ์ด ์กฐ์น˜๋กœ๋Š” ์˜์›ํžˆ ์•ˆ ๋œ๋‹ค โ€” ์ง€์‹œ ๋‚ด์šฉ์ด ํ‹€๋ ธ๋‹ค
193
+ *
194
+ * ๊ฐ€๋ฅด๋Š” ์ž๋ฆฌ๋Š” ใ€Œ์กฐ์น˜๋ฅผ ๋‹ค์‹œ ๋‚ผ ํ•„์š”๊ฐ€ ์žˆ๋‚˜ใ€๋‹ค. ์„ค์ •์ด ํ‹€๋ฆฐ ๊ฒƒ์€ ์กฐ์น˜๊ฐ€ ๋ฉ€์ฉกํ•˜๋ฏ€๋กœ `after-fix`,
195
+ * ์ง€์‹œ ๋‚ด์šฉ์ด ํ‹€๋ฆฐ ๊ฒƒ์€ ๊ทธ ์กฐ์น˜๊ฐ€ ์˜์›ํžˆ ํ‹€๋ ธ์œผ๋ฏ€๋กœ `never` ๋‹ค(์ธํ‹ฐ๊ทธ๋ ˆ์ด์…˜ ๋ ˆ์ธ ํŒ๋‹จ).
196
+ *
197
+ * **๋น„์–ด ์žˆ์œผ๋ฉด `never` ๋กœ ์ฝ์ง€ ์•Š๋Š”๋‹ค.** ๋ชจ๋ฅด๋Š” ๊ฒƒ๊ณผ ์˜์›ํžˆ ์•ˆ ๋˜๋Š” ๊ฒƒ์€ ๋‹ค๋ฅด๋‹ค โ€” ์„ ์–ธํ•˜์ง€ ์•Š์€
198
+ * ์ปค๋„ฅํ„ฐ์˜ ์‹คํŒจ๋ฅผ `never` ๋กœ ์ ‘์œผ๋ฉด ๊ณ ์น  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์„ ์‚ฌ๋žŒ์ด ํฌ๊ธฐํ•œ๋‹ค.
199
+ */
200
+ @Column({ nullable: true })
201
+ @Field({ nullable: true, description: 'Whether retrying can work: again ยท after-fix ยท never. Empty means the adapter did not say.' })
202
+ retry?: string
203
+
204
+ /** ๋ช‡ ๋ฒˆ ๋„˜๊ฒจ ๋ดค๋‚˜ โ€” ๋ชฉ๋ก์˜ ใ€Œ๋ช‡ ๋ฒˆ ํ•ด ๋ดค๋‚˜ใ€๊ฐ€ ์ด ๊ฐ’์ด๋‹ค. ์ฒซ ์‹œ๋„๊ฐ€ 1์ด๋‹ค. */
205
+ @Column({ type: 'int', nullable: true })
206
+ @Field(type => Int, { nullable: true, description: 'How many dispatch attempts have been made. The first attempt is 1.' })
207
+ attempts?: number
208
+
209
+ /**
210
+ * **๋‹ค์Œ์— ๋„˜๊ฒจ ๋ณผ ์‹œ๊ฐ**(ISO) โ€” ๋ฌผ๋Ÿฌ์„ฌ์„ ํ‘œ๋กœ ๋“ ๋‹ค.
211
+ *
212
+ * ๋ฉ”๋ชจ๋ฆฌ์— ๋‘์ง€ ์•Š๋Š” ์ด์œ : ์žฌ๊ธฐ๋™ํ•˜๋ฉด ์‚ฌ๋ผ์ง€๊ณ , ๊ทธ๋Ÿฌ๋ฉด ๋ฌผ๋Ÿฌ์„œ๋˜ ๊ฒƒ์ด ์ „๋ถ€ ์ฆ‰์‹œ ์žฌ์‹œ๋„๊ฐ€ ๋œ๋‹ค.
213
+ * ํ‘œ์— ๋‘๋ฉด ์ผ๊พผ์ด ์—ฌ๋Ÿฌ ๋Œ€์—ฌ๋„ ๊ฐ™์€ ๋‹ต์„ ๋ณธ๋‹ค.
214
+ *
215
+ * ๋น„์–ด ์žˆ์œผ๋ฉด **๋‹ค์Œ ์ฃผ๊ธฐ์— ๋ฐ”๋กœ** ๋ณธ๋‹ค(ํ•œ ๋ฒˆ๋„ ์‹คํŒจํ•˜์ง€ ์•Š์€ ๊ฒƒ).
216
+ */
217
+ @Column({ nullable: true })
218
+ @Field({ nullable: true, description: 'When to try dispatching again (ISO 8601). Empty means the next cycle.' })
219
+ nextAttemptAt?: string
220
+
221
+ @Column({ nullable: true })
222
+ @Field({ nullable: true, description: 'Why the dispatch failed. The server sentence, kept as it came.' })
223
+ error?: string
224
+
225
+ @CreateDateColumn()
226
+ @Field({ nullable: true, description: 'When the command was proposed.' })
227
+ createdAt?: Date
228
+
229
+ @UpdateDateColumn()
230
+ @Field({ nullable: true, description: 'When the row last changed.' })
231
+ updatedAt?: Date
232
+ }