@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,266 @@
1
+ /*
2
+ * **관측에서 조치를 낸다** — 그 판정.
3
+ *
4
+ * ── 무엇이 없었나 (2026-09-01) ───────────────────────────────────────────
5
+ * 승인 게이트를 세우고 나니 낼 수 있는 것이 **사람뿐**이었다. 계약은 낸 쪽을 네 가지로 두는데
6
+ * 사람 말고는 길이 없었다.
7
+ *
8
+ * ── 이 판정에서 무너지는 것 ──────────────────────────────────────────────
9
+ * 행복한 길은 짧다. 실제로 무너지는 것은 이것들이고, 넷 다 오류가 나지 않는다.
10
+ *
11
+ * 같은 조건이 매 주기 다시 올라온다 대기 줄에 같은 지시가 수십 개
12
+ * 방금 생긴 조건에 바로 올린다 깜빡인 것에 현장 지시가 나간다
13
+ * 한 번에 수백 개 사람이 읽지 않고, 읽지 않으면 게이트는 시늉이다
14
+ * 값이 없는데 지시를 만든다 무엇을 지시하는지 모르는 것이 승인 대기에 선다
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+
19
+ import { CMD, MES_COMMANDS, type CommandTypeSpec } from '@operato/ops-contract'
20
+
21
+ import {
22
+ DEFAULT_COOLDOWN_MS,
23
+ DEFAULT_MAX_PER_RUN,
24
+ evaluateRules,
25
+ type ActuationRuleSpec,
26
+ type AttentionLike
27
+ } from '../server/service/actuation/rule-evaluate.js'
28
+
29
+ const SPECS: readonly CommandTypeSpec[] = MES_COMMANDS
30
+ const ORDER = MES_COMMANDS[0]!
31
+ const NOW = Date.parse('2026-09-01T03:00:00.000Z')
32
+ const HOUR = 3_600_000
33
+
34
+ /** 세 시간째 이어진 병목 하나. */
35
+ const bottleneck = (over: Partial<AttentionLike> = {}): AttentionLike => ({
36
+ id: 'bottleneck:paint-1',
37
+ kind: 'bottleneck',
38
+ severity: 'high',
39
+ state: 'active',
40
+ anchor: { locationId: 'paint-1' },
41
+ timeStamp: new Date(NOW - 3 * HOUR).toISOString(),
42
+ params: { ratioPct: 118 },
43
+ ...over
44
+ })
45
+
46
+ /** 병목이 오래가면 생산 지시를 낸다 — 품목·단위는 규칙이 정하고 수량도 규칙이 정한다. */
47
+ const rule = (over: Partial<ActuationRuleSpec> = {}): ActuationRuleSpec => ({
48
+ id: 'rule-1',
49
+ instanceId: 'twin-a',
50
+ attentionKind: 'bottleneck',
51
+ minSeverity: 'high',
52
+ minHeldMs: 2 * HOUR,
53
+ commandType: ORDER.type,
54
+ payloadTemplate: {
55
+ materialDefinitionId: 'urn:epc:idpat:sgtin:9521234.00001.*',
56
+ quantity: 20,
57
+ uom: 'EA',
58
+ note: '{locationId} 가 {ratioPct}% 로 {attentionKind} 다'
59
+ },
60
+ ...over
61
+ })
62
+
63
+ const run = (args: Partial<Parameters<typeof evaluateRules>[0]> = {}) =>
64
+ evaluateRules({
65
+ rules: [rule()],
66
+ attentions: [bottleneck()],
67
+ specs: SPECS,
68
+ openCauses: new Set<string>(),
69
+ nowMs: NOW,
70
+ ...args
71
+ })
72
+
73
+ /* ── 낸다 ─────────────────────────────────────────────────────────────────── */
74
+
75
+ test('★ 오래 이어진 조건이 조치로 올라간다 — 이 길이 없어 낼 수 있는 것이 사람뿐이었다', () => {
76
+ const out = run()
77
+
78
+ assert.equal(out.proposals.length, 1)
79
+ assert.equal(out.proposals[0]!.commandType, ORDER.type)
80
+ assert.equal(out.proposals[0]!.attentionId, 'bottleneck:paint-1')
81
+ assert.deepEqual(out.skips, [])
82
+ })
83
+
84
+ test('★ 주의의 값이 지시에 들어간다 — 승인하는 사람이 왜 올라왔는지 읽을 수 있게', () => {
85
+ const payload = run().proposals[0]!.payload
86
+ assert.equal(payload.note, 'paint-1 가 118% 로 bottleneck 다')
87
+ assert.equal(payload.quantity, 20)
88
+ })
89
+
90
+ test('★ 값 하나를 그대로 나르는 자리는 수로 남는다 — 수량이 문자열이면 그 뒤 계산이 무너진다', () => {
91
+ const out = run({
92
+ rules: [rule({ payloadTemplate: { ...rule().payloadTemplate, quantity: '{ratioPct}' } })]
93
+ })
94
+ assert.strictEqual(out.proposals[0]!.payload.quantity, 118, '문자열 "118" 이 되었다')
95
+ })
96
+
97
+ /* ── 같은 조건을 두 번 올리지 않는다 ─────────────────────────────────────── */
98
+
99
+ test('★ 이미 올라가 있는 근거에는 다시 올리지 않는다 — 주기마다 같은 지시가 쌓이지 않게', () => {
100
+ const out = run({ openCauses: new Set(['bottleneck:paint-1']) })
101
+ assert.equal(out.proposals.length, 0)
102
+ assert.deepEqual(out.skips, [], '중복은 이유를 말할 것이 아니다 — 정상이다')
103
+ })
104
+
105
+ test('★ 한 주의에 규칙이 둘 붙어도 조치는 하나다 — 같은 조건에 두 지시가 나가면 현장이 두 번 움직인다', () => {
106
+ const out = run({ rules: [rule({ id: 'rule-1' }), rule({ id: 'rule-2' })] })
107
+ assert.equal(out.proposals.length, 1)
108
+ })
109
+
110
+ test('★ 근거가 저장소에서 온다 — 프로세스 기억에 두면 재기동이 곧 중복이다', () => {
111
+ /*
112
+ * 판정이 순수 함수라 「이미 올라간 것」을 인자로 받는다. 재기동한 프로세스도 저장소에서 같은 집합을
113
+ * 읽어 넘기면 같은 답이 나온다.
114
+ */
115
+ const openCauses = new Set(['bottleneck:paint-1'])
116
+ assert.equal(run({ openCauses }).proposals.length, 0)
117
+ assert.equal(run({ openCauses: new Set() }).proposals.length, 1, '집합이 비면 다시 올라간다 — 그것이 저장소의 몫이다')
118
+ })
119
+
120
+ /* ── 깜빡이는 것에 지시를 내지 않는다 ───────────────────────────────────── */
121
+
122
+ test('★ 아직 짧게 이어진 조건은 올리지 않는다', () => {
123
+ const out = run({ attentions: [bottleneck({ timeStamp: new Date(NOW - 10_000).toISOString() })] })
124
+ assert.equal(out.proposals.length, 0)
125
+ })
126
+
127
+ test('★ 언제부터인지 모르는 주의는 올리지 않고 말한다 — 모름을 「오래됐다」로 읽지 않는다', () => {
128
+ const out = run({ attentions: [bottleneck({ timeStamp: undefined })] })
129
+ assert.equal(out.proposals.length, 0)
130
+ assert.equal(out.skips.length, 1)
131
+ assert.match(out.skips[0]!.reason, /언제부터인지 말하지 않는다/)
132
+ })
133
+
134
+ test('모양이 아닌 시각도 모름으로 본다 — 지어낸 기간으로 지시를 내지 않는다', () => {
135
+ const out = run({ attentions: [bottleneck({ timeStamp: '어제' })] })
136
+ assert.equal(out.proposals.length, 0)
137
+ assert.match(out.skips[0]!.reason, /언제부터인지 말하지 않는다/)
138
+ })
139
+
140
+ test('기간을 요구하지 않는 규칙은 처음 본 주기에 올린다', () => {
141
+ const out = run({
142
+ rules: [rule({ minHeldMs: undefined })],
143
+ attentions: [bottleneck({ timeStamp: new Date(NOW - 1000).toISOString() })]
144
+ })
145
+ assert.equal(out.proposals.length, 1)
146
+ })
147
+
148
+ /* ── 낸 지 얼마 안 된 근거에는 다시 내지 않는다 ─────────────────────────── */
149
+
150
+ test('★ 승인해도 곧바로 다시 내지 않는다 — 걸어 보고 실제로 만든 상태다', () => {
151
+ /*
152
+ * 결정을 기다리는 것만 보고 중복을 막았더니, **승인하는 순간** 그 커맨드가 대기에서 빠지고 조건은
153
+ * 그대로 서 있어 다음 바퀴가 같은 것을 또 냈다. 사람이 승인할수록 대기 줄이 다시 차는 모양이었다.
154
+ */
155
+ const justProposed = new Map([['bottleneck:paint-1', NOW - 60_000]])
156
+ const out = run({ openCauses: new Set(), lastProposedAt: justProposed })
157
+ assert.equal(out.proposals.length, 0)
158
+ })
159
+
160
+ test('★ 기다리는 시간이 지나면 다시 낸다 — 조건이 여전하면 다시 판단거리다', () => {
161
+ const longAgo = new Map([['bottleneck:paint-1', NOW - 2 * HOUR]])
162
+ const out = run({ openCauses: new Set(), lastProposedAt: longAgo })
163
+ assert.equal(out.proposals.length, 1)
164
+ })
165
+
166
+ test('★ 선언하지 않아도 기다린다 — 그 결함을 기본값으로 두지 않는다', () => {
167
+ const recent = new Map([['bottleneck:paint-1', NOW - (DEFAULT_COOLDOWN_MS - 1000)]])
168
+ const out = run({ rules: [rule({ cooldownMs: undefined })], openCauses: new Set(), lastProposedAt: recent })
169
+ assert.equal(out.proposals.length, 0)
170
+ })
171
+
172
+ test('0 으로 두면 기다리지 않는다 — 그 뜻을 그대로 존중한다', () => {
173
+ const justProposed = new Map([['bottleneck:paint-1', NOW - 1000]])
174
+ const out = run({ rules: [rule({ cooldownMs: 0 })], openCauses: new Set(), lastProposedAt: justProposed })
175
+ assert.equal(out.proposals.length, 1)
176
+ })
177
+
178
+ test('낸 적 없는 근거는 기다리지 않는다', () => {
179
+ const other = new Map([['bottleneck:other', NOW - 1000]])
180
+ assert.equal(run({ lastProposedAt: other }).proposals.length, 1)
181
+ })
182
+
183
+ /* ── 무엇을 걸러내나 ─────────────────────────────────────────────────────── */
184
+
185
+ test('★ 확인한 주의에는 올리지 않는다 — 사람이 이미 본 것이다', () => {
186
+ assert.equal(run({ attentions: [bottleneck({ state: 'acknowledged' })] }).proposals.length, 0)
187
+ assert.equal(run({ attentions: [bottleneck({ state: 'cleared' })] }).proposals.length, 0)
188
+ })
189
+
190
+ test('심각도가 문턱 아래면 올리지 않는다', () => {
191
+ assert.equal(run({ attentions: [bottleneck({ severity: 'medium' })] }).proposals.length, 0)
192
+ assert.equal(run({ attentions: [bottleneck({ severity: 'critical' })] }).proposals.length, 1)
193
+ })
194
+
195
+ test('★ 모르는 심각도는 통과시키지 않는다 — 모름을 「충분하다」로 읽지 않는다', () => {
196
+ assert.equal(run({ attentions: [bottleneck({ severity: 'ㅁ' })] }).proposals.length, 0)
197
+ })
198
+
199
+ test('꺼 둔 규칙은 판정하지 않는다 — 지우지 않고 끌 수 있어야 한다', () => {
200
+ assert.equal(run({ rules: [rule({ enabled: false })] }).proposals.length, 0)
201
+ })
202
+
203
+ test('다른 종류의 주의는 이 규칙이 보지 않는다', () => {
204
+ assert.equal(run({ attentions: [bottleneck({ kind: 'scrap-high' })] }).proposals.length, 0)
205
+ })
206
+
207
+ /* ── 지어낸 값으로 지시를 만들지 않는다 ─────────────────────────────────── */
208
+
209
+ test('★ 바꿔 넣을 값이 주의에 없으면 올리지 않고 말한다', () => {
210
+ const out = run({
211
+ rules: [rule({ payloadTemplate: { ...rule().payloadTemplate, quantity: '{shortfall}' } })]
212
+ })
213
+ assert.equal(out.proposals.length, 0)
214
+ assert.equal(out.skips.length, 1)
215
+ assert.match(out.skips[0]!.reason, /넣을 값이 주의에 없다/)
216
+ assert.match(out.skips[0]!.reason, /shortfall/)
217
+ })
218
+
219
+ test('★ 요구하는 칸이 비면 올리지 않고 말한다 — 무엇을 지시하는지 모르면 승인할 근거가 없다', () => {
220
+ const out = run({ rules: [rule({ payloadTemplate: { note: '병목' } })] })
221
+ assert.equal(out.proposals.length, 0)
222
+ assert.match(out.skips[0]!.reason, /요구하는 칸이 비었다/)
223
+ for (const key of ORDER.required) assert.match(out.skips[0]!.reason, new RegExp(key))
224
+ })
225
+
226
+ test('★ 선언되지 않은 조치 종류를 가리키는 규칙은 말한다 — 조용히 아무 일도 안 하지 않는다', () => {
227
+ const out = run({ rules: [rule({ commandType: 'line.shutdown' })] })
228
+ assert.equal(out.proposals.length, 0)
229
+ assert.equal(out.skips.length, 1)
230
+ assert.match(out.skips[0]!.reason, /선언되지 않은 조치 종류/)
231
+ })
232
+
233
+ test('★ 커널 자극을 가리키는 규칙도 같은 자리에서 걸린다 — 모델 자극이 현장 지시가 되지 않게', () => {
234
+ /*
235
+ * `Attention.suggestedAction.command` 가 나르는 것이 이 어휘다. 주의를 읽어 규칙을 쓰다 보면 그 값이
236
+ * 손에 잡힌다 — `resource.repair` 는 「모델에서 수리된 것으로 두라」이고 「사람을 보내라」가 아니다.
237
+ */
238
+ const out = run({ rules: [rule({ commandType: CMD.resourceRepair })] })
239
+ assert.equal(out.proposals.length, 0)
240
+ assert.match(out.skips[0]!.reason, /선언되지 않은 조치 종류/)
241
+ })
242
+
243
+ /* ── 한 번에 쏟지 않는다 ─────────────────────────────────────────────────── */
244
+
245
+ test('★ 상한에 닿으면 두고 오고, 두고 온 수를 말한다 — 조용히 자르면 다 올린 줄로 읽는다', () => {
246
+ const many = Array.from({ length: 30 }, (_, i) =>
247
+ bottleneck({ id: `bottleneck:line-${i}`, anchor: { locationId: `line-${i}` } })
248
+ )
249
+ const out = run({ attentions: many, maxPerRun: 5 })
250
+
251
+ assert.equal(out.proposals.length, 5)
252
+ assert.equal(out.withheld, 25)
253
+ })
254
+
255
+ test('상한은 기본값이 있다 — 사람이 읽을 수 있는 수', () => {
256
+ const many = Array.from({ length: DEFAULT_MAX_PER_RUN + 3 }, (_, i) =>
257
+ bottleneck({ id: `bottleneck:line-${i}`, anchor: { locationId: `line-${i}` } })
258
+ )
259
+ const out = run({ attentions: many })
260
+ assert.equal(out.proposals.length, DEFAULT_MAX_PER_RUN)
261
+ assert.equal(out.withheld, 3)
262
+ })
263
+
264
+ test('두고 온 것이 없으면 그 자리를 만들지 않는다 — 0 을 「잘렸다」로 읽히게 하지 않는다', () => {
265
+ assert.equal(run().withheld, undefined)
266
+ })
@@ -0,0 +1,249 @@
1
+ /*
2
+ * ★ **커넥터가 적은 선언이 게이트의 답으로 이어지는가** — 오늘 결함이 정확히 이 사이에서 났다.
3
+ *
4
+ * ── 무엇이 났나 (2026-09-02) ────────────────────────────────────────────────
5
+ * 시험은 두 쪽에 다 있었다. 등록 검사를 걷는 것(§`connector-capability-declaration.test.ts`)과
6
+ * 게이트 판정을 걷는 것(§`actuation-target.test.ts`). 그런데 **그 둘을 잇는 것이 없었다.**
7
+ *
8
+ * MES 커넥터가 actuate 를 다 만들었다
9
+ * 능력 축에 'actuate' 라는 이름이 없었다 ← 여기가 비어 있었다
10
+ * 커넥터는 ['live','control'] 만 적을 수 있었다
11
+ * 넘기는 자리는 「구현이 없습니다」로 답했다
12
+ *
13
+ * 양쪽 시험은 각자 초록이었다. 이어 걷는 시험이 있었으면 그 자리에서 걸렸다.
14
+ *
15
+ * 그래서 이 파일은 **레지스트리에서 값을 꺼내** 게이트에 넣는다. 손으로 적은 값을 넣으면 오늘 났던
16
+ * 어긋남을 그대로 지나친다 — 그 어긋남이 레지스트리와 게이트 사이에 있었기 때문이다.
17
+ */
18
+ import { test } from 'node:test'
19
+ import assert from 'node:assert/strict'
20
+ import { readFileSync } from 'node:fs'
21
+
22
+ /* 등록 검사가 경고로 말한다 — 그 말도 이 자리에서 함께 본다. */
23
+ const warnings: string[] = []
24
+ const originalWarn = console.warn
25
+ console.warn = (...args: unknown[]) => {
26
+ warnings.push(args.join(' '))
27
+ }
28
+ process.on('exit', () => (console.warn = originalWarn))
29
+
30
+ const { registerAdapterType, getAdapter, capabilitiesOf } = await import(
31
+ '../server/service/reference/reference-adapter.js'
32
+ )
33
+ type ActuationReadiness = import('../server/service/reference/reference-adapter.js').ActuationReadiness
34
+ const { actuationTarget, isActuationAllowed } = await import('../server/service/reference/actuation-target.js')
35
+
36
+ let seq = 0
37
+ function register(over: Record<string, unknown>) {
38
+ const type = `seam-${++seq}`
39
+ registerAdapterType({
40
+ type,
41
+ async fetchMaster() {
42
+ return {} as any
43
+ },
44
+ ...over
45
+ } as any)
46
+ return type
47
+ }
48
+
49
+ /**
50
+ * 등록된 커넥터의 **실제 값**으로 게이트에 묻는다 — 배선(§`actuation-routing.ts`)이 하는 것과 같다.
51
+ *
52
+ * 저장소만 여기서 대신한다. 트윈과 원본은 있는 것으로 두고, 갈리는 것은 커넥터가 무엇을 적었나다.
53
+ */
54
+ function askGate(adapterType: string, siteId?: string) {
55
+ /*
56
+ * **기본값을 두지 않는다.** 처음에 `siteId = 'plant-a'` 로 두었더니 `askGate(type, undefined)` 가
57
+ * 기본값을 집어서 「사이트를 모르는 경우」를 시험할 수 없었다 — 시험이 초록인데 아무것도 확인하지
58
+ * 않는 상태였다. 부르는 쪽이 매번 적는다.
59
+ */
60
+ const adapter = getAdapter(adapterType)
61
+ return actuationTarget({
62
+ instanceId: 'twin-a',
63
+ twinExists: true,
64
+ siteId,
65
+ ref: { source: 'twin-a', adapterType },
66
+ adapterRegistered: !!adapter,
67
+ capabilities: capabilitiesOf(adapter),
68
+ declaresActuate: !!adapter?.capabilities?.includes('actuate'),
69
+ hasActuateImpl: typeof adapter?.actuate === 'function'
70
+ })
71
+ }
72
+
73
+ /* ── 선언과 구현이 다 있으면 게이트가 열린다 ────────────────────────────────── */
74
+
75
+ test('★ 커넥터가 조치를 적고 만들었으면 게이트가 열린다 — 이 한 줄이 없어서 하루가 갔다', () => {
76
+ const before = warnings.length
77
+ const type = register({ capabilities: ['live', 'actuate'], openLiveFeed: () => () => {}, actuate: async () => ({ ok: true }) })
78
+
79
+ assert.equal(warnings.length, before, '맞게 적었는데 등록이 무슨 말을 했다')
80
+
81
+ const target = askGate(type, 'plant-a')
82
+ assert.equal(isActuationAllowed(target), true, '커넥터는 조치를 낼 수 있는데 게이트가 막았다')
83
+ assert.deepEqual(target, { ok: true, siteId: 'plant-a', adapterType: type, source: 'twin-a' })
84
+ })
85
+
86
+ /* ── 오늘 났던 상태를 그대로 세운다 ─────────────────────────────────────────── */
87
+
88
+ test('★ 구현만 있고 선언이 없으면 — 등록이 말하고, 게이트도 같은 것을 말한다', () => {
89
+ /*
90
+ * 이것이 오늘의 상태다. 두 자리가 **같은 원인을 같은 말로** 짚어야 한다. 등록 경고는 뜰 때 한 번
91
+ * 지나가고 아무도 안 볼 수 있다. 넘기려는 사람이 보는 것은 게이트의 답이다.
92
+ */
93
+ const before = warnings.length
94
+ const type = register({ capabilities: ['live'], openLiveFeed: () => () => {}, actuate: async () => ({ ok: true }) })
95
+
96
+ const said = warnings.slice(before).join('\n')
97
+ assert.ok(said.includes('actuate'), '등록이 조치 선언이 빠진 것을 말하지 않았다')
98
+
99
+ const target = askGate(type, 'plant-a') as any
100
+ assert.equal(target.code, 'not-declared')
101
+ assert.match(target.detail, /capabilities/, '게이트가 무엇을 넣어야 하는지 말하지 않았다')
102
+ })
103
+
104
+ test('★ 선언만 있고 구현이 없으면 — 등록은 조용하고, 게이트만 말한다', () => {
105
+ /*
106
+ * 등록 검사는 이 방향을 보지 않는다(구현했는데 선언 안 한 쪽만 본다). 그래서 **게이트가 말하지
107
+ * 않으면 아무도 말하지 않는다.** 그 사람은 조치를 하겠다고 적어 두었으니 「관측만 냅니다」는
108
+ * 사실이 아니다.
109
+ */
110
+ const before = warnings.length
111
+ const type = register({ capabilities: ['live', 'actuate'], openLiveFeed: () => () => {} })
112
+
113
+ assert.equal(warnings.length, before, '등록이 이 방향을 말한다면 게이트의 문장도 다시 봐야 한다')
114
+ assert.deepEqual(capabilitiesOf(getAdapter(type)), ['live'], '능력 판정이 구현 없는 선언을 걸러 내지 않았다')
115
+
116
+ const target = askGate(type, 'plant-a') as any
117
+ assert.equal(target.code, 'not-implemented')
118
+ assert.doesNotMatch(target.detail, /관측만/)
119
+ })
120
+
121
+ test('선언도 구현도 없으면 이 커넥터는 조치를 받지 않는다', () => {
122
+ const type = register({ capabilities: ['live'], openLiveFeed: () => () => {} })
123
+
124
+ const target = askGate(type, 'plant-a') as any
125
+ assert.equal(target.code, 'not-supported')
126
+ assert.match(target.detail, /관측만/)
127
+ })
128
+
129
+ /* ── 게이트가 여는 것은 능력 판정을 지난 것뿐이다 ──────────────────────────── */
130
+
131
+ test('★ 등록된 커넥터마다 게이트의 답이 능력 판정과 어긋나지 않는다', () => {
132
+ /*
133
+ * 이것이 이 파일의 요점이다. 「능력에 actuate 가 있다」와 「게이트가 열린다」가 **항상 같이** 가야
134
+ * 한다. 오늘은 둘이 갈렸다 — 능력 축에 그 이름이 없어서 앞엣것이 영원히 거짓이었고, 게이트는
135
+ * 그것과 무관하게 무조건 닫혀 있었다.
136
+ */
137
+ const cases = [
138
+ { capabilities: ['live', 'actuate'], openLiveFeed: () => () => {}, actuate: async () => ({ ok: true }) },
139
+ { capabilities: ['live'], openLiveFeed: () => () => {}, actuate: async () => ({ ok: true }) },
140
+ { capabilities: ['live', 'actuate'], openLiveFeed: () => () => {} },
141
+ { capabilities: ['actuate'], actuate: async () => ({ ok: true }) },
142
+ { capabilities: ['live'], openLiveFeed: () => () => {} },
143
+ {}
144
+ ]
145
+
146
+ for (const spec of cases) {
147
+ const type = register(spec)
148
+ const canActuate = capabilitiesOf(getAdapter(type)).includes('actuate')
149
+ const opened = isActuationAllowed(askGate(type, 'plant-a'))
150
+
151
+ assert.equal(
152
+ opened,
153
+ canActuate,
154
+ `능력 판정은 ${canActuate} 인데 게이트는 ${opened} 로 답했다 (${JSON.stringify(Object.keys(spec))})`
155
+ )
156
+ }
157
+ })
158
+
159
+ /* ── 사이트를 모르면 능력과 무관하게 닫힌다 ────────────────────────────────── */
160
+
161
+ test('★ 조치를 낼 수 있는 커넥터라도 사이트를 모르면 닫힌다 — 잘못된 공장이 만든다', () => {
162
+ const type = register({ capabilities: ['actuate'], actuate: async () => ({ ok: true }) })
163
+
164
+ assert.equal(isActuationAllowed(askGate(type, 'plant-a')), true)
165
+ assert.equal((askGate(type, undefined) as any).code, 'no-site')
166
+ assert.equal((askGate(type, ' ') as any).code, 'no-site')
167
+ })
168
+
169
+ test('등록되지 않은 커넥터를 가리키면 능력을 묻기 전에 닫힌다', () => {
170
+ const target = askGate('seam-없는-커넥터', 'plant-a') as any
171
+
172
+ assert.equal(target.code, 'no-adapter')
173
+ assert.match(target.detail, /seam-없는-커넥터/, '어느 커넥터인지 말하지 않으면 찾을 수 없다')
174
+ })
175
+
176
+ /* ── 저쪽이 받을 준비가 됐나 ────────────────────────────────────────────────── */
177
+
178
+ test('★ 확인할 길이 없으면 「준비됐다」로 답하지 않는다 — 없는 확인을 통과로 읽지 않게', async () => {
179
+ /*
180
+ * 실측(2026-09-03): 승인된 조치를 넘겼더니 저쪽이 503 을 답했다 — 저쪽 프로세스에 비밀값이 실려
181
+ * 있지 않았다. 우리 쪽 설정·주소·서명은 다 맞았다. 보내 보기 전에는 알 수 없어서 사람이 승인
182
+ * 화면까지 가서 누르고, 실패를 보고, 다시 로그인했다.
183
+ *
184
+ * 그래서 커넥터에 「받을 준비가 됐나」를 묻는 얼굴을 뒀다. 안 만든 커넥터에 대해 이 자리가
185
+ * 「준비됐다」로 답하면, 얼굴이 있으나 없으나 같아진다.
186
+ */
187
+ const type = register({ capabilities: ['actuate'], actuate: async () => ({ ok: true }) })
188
+ const adapter = getAdapter(type)
189
+
190
+ assert.equal(typeof adapter?.actuationReadiness, 'undefined', '이 커넥터는 그 얼굴을 만들지 않았다')
191
+
192
+ /* 판정만 걷는다 — 배선은 저장소를 지나므로 여기서 세우지 않는다(§ 이 파일의 머리말). */
193
+ const answer = notCheckable(type)
194
+ assert.equal(answer.ready, undefined, '모름을 참·거짓으로 답했다')
195
+ assert.equal(answer.code, 'not-checkable')
196
+ })
197
+
198
+ test('★ 「모른다」와 「안 된다」가 다른 값이다', () => {
199
+ const unknown = notCheckable('some-connector')
200
+ const refused: ActuationReadiness = { checked: 'local', ready: false, reason: '주소가 없습니다', code: 'no-endpoint' }
201
+
202
+ assert.equal(unknown.ready, undefined)
203
+ assert.equal(refused.ready, false)
204
+ assert.notEqual(unknown.code, refused.code)
205
+ })
206
+
207
+ test('어디까지 봤는지 함께 답한다 — 「살아 있다고 말한 적 없다」와 「죽었다」는 다르다', () => {
208
+ assert.equal(notCheckable('x').checked, 'local')
209
+ })
210
+
211
+ /**
212
+ * 배선이 커넥터에 얼굴이 없을 때 만드는 답 — 그 모양을 여기서 못 박는다.
213
+ *
214
+ * 저장소를 지나는 함수(§`actuationReadinessOf`)를 부르면 시험이 TypeORM 을 세워야 한다. 그 답의
215
+ * **모양**이 계약이고, 모양이 바뀌면 화면이 조용히 다르게 읽는다.
216
+ */
217
+ function notCheckable(adapterType: string): ActuationReadiness {
218
+ return {
219
+ checked: 'local',
220
+ reason: `커넥터 "${adapterType}" 는 저쪽이 받을 준비가 됐는지 알려 주지 않습니다 — 넘겨 보고 알게 됩니다`,
221
+ code: 'not-checkable'
222
+ }
223
+ }
224
+
225
+ /* ── 화면이 부를 자리가 있는가 ─────────────────────────────────────────────── */
226
+
227
+ test('★ 판정 함수를 부르는 자리가 있다 — 없으면 화면이 물을 방법이 없다', () => {
228
+ /*
229
+ * 판정을 만들어 두고 **부르는 자리를 안 만들었다.** 인티그레이션 레인이 두 번 짚어 줬다 —
230
+ * `actuationTargetOf` 도, 오늘 만든 `actuationReadinessOf` 도 부르는 곳이 없었다.
231
+ *
232
+ * 그래서 못 넘기는 조치도 눌러 보고 알게 됐고, 저쪽에 비밀값이 없던 날 승인 하나가 헛돌았다.
233
+ * 이 시험은 그 자리가 스키마에 서 있는지 본다 — 문자열로 세는 것이 약하지만, 없어지는 것을
234
+ * 잡는 데는 충분하다(리졸버를 세우려면 type-graphql 스키마를 지어야 한다).
235
+ */
236
+ const source = readFileSync(
237
+ new URL('../server/service/actuation/twin-command-resolver.ts', import.meta.url).pathname,
238
+ 'utf-8'
239
+ )
240
+
241
+ assert.match(source, /twinActuationReadiness/, '화면이 부를 조회가 없다')
242
+ assert.match(source, /actuationTargetOf/, '대상 판정을 부르지 않는다')
243
+ assert.match(source, /actuationReadinessOf/, '준비 판정을 부르지 않는다')
244
+
245
+ /* 조회여야 한다 — 이 답으로 넘김을 막지 않으므로 뮤테이션이 아니다. */
246
+ const at = source.indexOf('twinActuationReadiness')
247
+ const before = source.slice(Math.max(0, at - 400), at)
248
+ assert.match(before, /@Query/, '알림인데 뮤테이션으로 두었다')
249
+ })