@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,63 @@
1
+ /** 어댑터가 답하는 세 갈래. 비어 있으면 **말하지 않은 것**이고, `never` 가 아니다. */
2
+ export type RetryOutlook = 'again' | 'after-fix' | 'never';
3
+ export declare function isRetryOutlook(v: unknown): v is RetryOutlook;
4
+ /**
5
+ * `again` 의 물러섬 — 30초 · 2분 · 10분, 그다음 접는다.
6
+ *
7
+ * 첫 값이 일꾼의 주기와 같다(§`RULE_PERIOD_MS`). 새 박자를 만들지 않는다.
8
+ */
9
+ export declare const AGAIN_BACKOFF_MS: readonly [30000, 120000, 600000];
10
+ /**
11
+ * `after-fix` 는 **10분마다 계속** 본다 — 접지 않는다(머리말).
12
+ *
13
+ * 사람이 설정을 고치는 것을 기다리는 간격이다. 짧게 두면 헛되고, 길게 두면 고쳐 놓고 기다린다.
14
+ */
15
+ export declare const AFTER_FIX_PERIOD_MS = 600000;
16
+ /** 왜 집지 않는가 — 목록이 「고칠 것」과 「우리 쪽을 보지 말 것」을 갈라 보이게 한다. */
17
+ export type SkipReason =
18
+ /** 종착이거나 넘길 수 없는 상태다. */
19
+ 'not-dispatchable'
20
+ /** 아직 물러섬이 끝나지 않았다 — 다음 기회를 기다린다. */
21
+ | 'backing-off'
22
+ /** 어댑터가 「영원히 안 된다」고 답했다 — 사람이 **새 조치**를 내야 한다. 우리 쪽에 고칠 것이 없다. */
23
+ | 'never'
24
+ /** `again` 을 세 번 다 썼다 — 사람이 봐야 한다. */
25
+ | 'attempts-spent'
26
+ /** 어댑터가 다시 될 일인지 말하지 않았다 — 모르는 것을 재시도로 밀지 않는다. */
27
+ | 'outlook-unknown'
28
+ /** 재시도인데 커넥터가 멱등을 선언하지 않았다 — 두 건이 설 위험을 우리가 지지 않는다. */
29
+ | 'not-idempotent';
30
+ export interface RetryDecision {
31
+ /** 지금 넘길 것인가. */
32
+ take: boolean;
33
+ /** 안 넘기면 왜 — 목록이 그대로 보인다. */
34
+ skip?: SkipReason;
35
+ /** 넘길 것이면 이번이 몇 번째 시도인가(첫 시도가 1). */
36
+ attempt?: number;
37
+ /** 이번 시도가 실패하면 다음은 언제인가(ISO). 없으면 더 안 본다. */
38
+ nextAttemptAt?: string;
39
+ }
40
+ export interface RetryInput {
41
+ /** 지금 상태 — `approved` 는 첫 시도, `failed` 는 재시도. */
42
+ state: string;
43
+ /** 넘길 수 있는 상태인가 — 상태 기계가 답한다(`nextCommandState(state,'dispatch')`). */
44
+ dispatchable: boolean;
45
+ /** 어댑터가 지난 실패에 답한 갈래. 첫 시도에는 없다. */
46
+ retry?: string;
47
+ /** 지금까지 몇 번 넘겨 봤나. 없으면 0. */
48
+ attempts?: number;
49
+ /** 다음에 볼 시각(ISO). 없으면 지금 봐도 된다. */
50
+ nextAttemptAt?: string;
51
+ /** 이 커넥터가 두 번 보내도 지시가 하나인가 — **선언만 참으로 읽는다.** */
52
+ idempotent?: boolean;
53
+ /** 지금(ms). */
54
+ nowMs: number;
55
+ }
56
+ /**
57
+ * 이 조치를 지금 넘길 것인가.
58
+ *
59
+ * **모르면 넘기지 않는다.** 어댑터가 갈래를 말하지 않았거나 멱등을 선언하지 않았으면 재시도하지
60
+ * 않고 목록에 남긴다 — 오늘 커널의 다른 축에서 지킨 것과 같은 규율이다(정격 사용량·품질 계수기·
61
+ * 조용한 시각: 모르면 단언하지 않는다).
62
+ */
63
+ export declare function retryDecisionOf(input: RetryInput): RetryDecision;
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ /*
3
+ * **다시 넘길 것인가, 언제인가** — 일꾼이 묻는 것 하나.
4
+ *
5
+ * ── 왜 순수한가 ─────────────────────────────────────────────────────────────
6
+ * 일꾼의 판단이 전부 여기 있다. DB 도 어댑터도 시계도 보지 않는다 — 시각은 받는다. 그래서 「10분
7
+ * 뒤에 다시」 같은 규칙을 기다리지 않고 값만 바꿔 가며 잴 수 있다.
8
+ *
9
+ * ── 세 갈래는 어댑터가 답한다 ───────────────────────────────────────────────
10
+ * 실패가 다 같지 않다. 2026-09-02 에 둘을 겪었다 — 저쪽 인증 정보가 없어 503(고치면 됨)이었고,
11
+ * `ITEM-1` 은 400(고쳐도 이 조치로는 안 됨)이었다. 답만 보면 구별되지 않았다.
12
+ *
13
+ * again 그대로 다시 해 볼 만하다 못 닿았거나 저쪽이 잠깐 흔들렸다
14
+ * after-fix 사람이 고친 뒤 그대로 나간다 설정 문제
15
+ * never 이 조치로는 영원히 안 된다 지시 내용이 틀렸다
16
+ *
17
+ * 가르는 자리는 **「조치를 다시 낼 필요가 있나」**다(인티그레이션 레인 판단).
18
+ *
19
+ * ── `after-fix` 를 횟수로 접지 않는다 ───────────────────────────────────────
20
+ * 이것이 이 파일에서 가장 중요한 결정이다. `after-fix` 는 **재시도가 아니라 기다리는 것**이다.
21
+ * 두드려서 풀릴 일이 아니고, 사람이 설정을 고치는 순간 다음 주기에 나가면 된다.
22
+ *
23
+ * 횟수로 접으면 **사람이 고쳐 놓고도 다시 손으로 눌러야** 한다. 2026-09-02 사고가 정확히 그
24
+ * 모양이었다 — 저쪽이 고쳐진 뒤 사람이 손으로 다시 넘겨서 나갔다.
25
+ *
26
+ * `again` 만 접는다. 끝없이 두드리는 것은 소음이고, 짧게 세 번 해서 안 되면 망이 흔들린 것이
27
+ * 아니라 다른 것이다 — 그때는 사람이 봐야 한다.
28
+ *
29
+ * ── 멱등이 첫 시도와 재시도를 가른다 ────────────────────────────────────────
30
+ * approved → 넘김 **첫 시도**다. 멱등 선언이 없어도 한다 — 한 번뿐이라 위험이 없다
31
+ * failed → 넘김 **재시도**다. 두 번 보내도 지시가 하나라고 선언한 커넥터만 한다
32
+ *
33
+ * 선언하지 않은 커넥터의 실패는 목록에 남는다. 사람이 저쪽 상태를 보고 결정해야 한다 — 우리가
34
+ * 「아마 괜찮겠지」로 두 번 보내면 현장에 지시가 두 건 선다.
35
+ */
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.AFTER_FIX_PERIOD_MS = exports.AGAIN_BACKOFF_MS = void 0;
38
+ exports.isRetryOutlook = isRetryOutlook;
39
+ exports.retryDecisionOf = retryDecisionOf;
40
+ function isRetryOutlook(v) {
41
+ return v === 'again' || v === 'after-fix' || v === 'never';
42
+ }
43
+ /**
44
+ * `again` 의 물러섬 — 30초 · 2분 · 10분, 그다음 접는다.
45
+ *
46
+ * 첫 값이 일꾼의 주기와 같다(§`RULE_PERIOD_MS`). 새 박자를 만들지 않는다.
47
+ */
48
+ exports.AGAIN_BACKOFF_MS = [30_000, 120_000, 600_000];
49
+ /**
50
+ * `after-fix` 는 **10분마다 계속** 본다 — 접지 않는다(머리말).
51
+ *
52
+ * 사람이 설정을 고치는 것을 기다리는 간격이다. 짧게 두면 헛되고, 길게 두면 고쳐 놓고 기다린다.
53
+ */
54
+ exports.AFTER_FIX_PERIOD_MS = 600_000;
55
+ const at = (ms) => new Date(ms).toISOString();
56
+ /**
57
+ * 이 조치를 지금 넘길 것인가.
58
+ *
59
+ * **모르면 넘기지 않는다.** 어댑터가 갈래를 말하지 않았거나 멱등을 선언하지 않았으면 재시도하지
60
+ * 않고 목록에 남긴다 — 오늘 커널의 다른 축에서 지킨 것과 같은 규율이다(정격 사용량·품질 계수기·
61
+ * 조용한 시각: 모르면 단언하지 않는다).
62
+ */
63
+ function retryDecisionOf(input) {
64
+ if (!input.dispatchable)
65
+ return { take: false, skip: 'not-dispatchable' };
66
+ const attempts = typeof input.attempts === 'number' && input.attempts > 0 ? input.attempts : 0;
67
+ /*
68
+ * **첫 시도** — 승인됐는데 한 번도 안 나간 것. 물러섬도 멱등도 묻지 않는다: 한 번뿐이라 두 건이
69
+ * 설 수 없고, 어댑터는 아직 아무 답도 하지 않았다.
70
+ *
71
+ * 이 갈래가 승인 직후 발신이 놓친 것을 줍는다 — 커밋과 발신 사이에 프로세스가 죽은 경우다.
72
+ */
73
+ if (attempts === 0) {
74
+ return { take: true, attempt: 1, nextAttemptAt: at(input.nowMs + exports.AGAIN_BACKOFF_MS[0]) };
75
+ }
76
+ /* 재시도다. 여기서부터 어댑터의 답과 커넥터의 성질을 묻는다. */
77
+ if (!isRetryOutlook(input.retry))
78
+ return { take: false, skip: 'outlook-unknown' };
79
+ if (input.retry === 'never')
80
+ return { take: false, skip: 'never' };
81
+ if (input.idempotent !== true)
82
+ return { take: false, skip: 'not-idempotent' };
83
+ if (input.nextAttemptAt) {
84
+ const dueMs = Date.parse(input.nextAttemptAt);
85
+ /* 읽을 수 없는 값으로 막지 않는다 — 막으면 그 조치가 표에 갇힌다. */
86
+ if (Number.isFinite(dueMs) && input.nowMs < dueMs)
87
+ return { take: false, skip: 'backing-off' };
88
+ }
89
+ if (input.retry === 'after-fix') {
90
+ /* 접지 않는다(머리말) — 같은 간격으로 계속 본다. */
91
+ return { take: true, attempt: attempts + 1, nextAttemptAt: at(input.nowMs + exports.AFTER_FIX_PERIOD_MS) };
92
+ }
93
+ /* `again` — 물러섬 표를 다 쓰면 접는다. */
94
+ if (attempts >= exports.AGAIN_BACKOFF_MS.length)
95
+ return { take: false, skip: 'attempts-spent' };
96
+ const next = exports.AGAIN_BACKOFF_MS[attempts];
97
+ return { take: true, attempt: attempts + 1, nextAttemptAt: at(input.nowMs + next) };
98
+ }
99
+ //# sourceMappingURL=retry-plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-plan.js","sourceRoot":"","sources":["../../../server/service/actuation/retry-plan.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;;;AAKH,wCAEC;AAoED,0CAoCC;AA1GD,SAAgB,cAAc,CAAC,CAAU;IACvC,OAAO,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,OAAO,CAAA;AAC5D,CAAC;AAED;;;;GAIG;AACU,QAAA,gBAAgB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAU,CAAA;AAEnE;;;;GAIG;AACU,QAAA,mBAAmB,GAAG,OAAO,CAAA;AA6C1C,MAAM,EAAE,GAAG,CAAC,EAAU,EAAU,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;AAE7D;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,KAAiB;IAC/C,IAAI,CAAC,KAAK,CAAC,YAAY;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,kBAAkB,EAAE,CAAA;IAEzE,MAAM,QAAQ,GAAG,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IAE9F;;;;;OAKG;IACH,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,wBAAgB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACzF,CAAC;IAED,uCAAuC;IACvC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAA;IACjF,IAAI,KAAK,CAAC,KAAK,KAAK,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;IAClE,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAA;IAE7E,IAAI,KAAK,CAAC,aAAa,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;QAC7C,4CAA4C;QAC5C,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,GAAG,KAAK;YAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,CAAA;IAChG,CAAC;IAED,IAAI,KAAK,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;QAChC,kCAAkC;QAClC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,GAAG,CAAC,EAAE,aAAa,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,2BAAmB,CAAC,EAAE,CAAA;IACpG,CAAC;IAED,gCAAgC;IAChC,IAAI,QAAQ,IAAI,wBAAgB,CAAC,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAA;IAEvF,MAAM,IAAI,GAAG,wBAAgB,CAAC,QAAQ,CAAC,CAAA;IACvC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,GAAG,CAAC,EAAE,aAAa,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,EAAE,CAAA;AACrF,CAAC","sourcesContent":["/*\n * **다시 넘길 것인가, 언제인가** — 일꾼이 묻는 것 하나.\n *\n * ── 왜 순수한가 ─────────────────────────────────────────────────────────────\n * 일꾼의 판단이 전부 여기 있다. DB 도 어댑터도 시계도 보지 않는다 — 시각은 받는다. 그래서 「10분\n * 뒤에 다시」 같은 규칙을 기다리지 않고 값만 바꿔 가며 잴 수 있다.\n *\n * ── 세 갈래는 어댑터가 답한다 ───────────────────────────────────────────────\n * 실패가 다 같지 않다. 2026-09-02 에 둘을 겪었다 — 저쪽 인증 정보가 없어 503(고치면 됨)이었고,\n * `ITEM-1` 은 400(고쳐도 이 조치로는 안 됨)이었다. 답만 보면 구별되지 않았다.\n *\n * again 그대로 다시 해 볼 만하다 못 닿았거나 저쪽이 잠깐 흔들렸다\n * after-fix 사람이 고친 뒤 그대로 나간다 설정 문제\n * never 이 조치로는 영원히 안 된다 지시 내용이 틀렸다\n *\n * 가르는 자리는 **「조치를 다시 낼 필요가 있나」**다(인티그레이션 레인 판단).\n *\n * ── `after-fix` 를 횟수로 접지 않는다 ───────────────────────────────────────\n * 이것이 이 파일에서 가장 중요한 결정이다. `after-fix` 는 **재시도가 아니라 기다리는 것**이다.\n * 두드려서 풀릴 일이 아니고, 사람이 설정을 고치는 순간 다음 주기에 나가면 된다.\n *\n * 횟수로 접으면 **사람이 고쳐 놓고도 다시 손으로 눌러야** 한다. 2026-09-02 사고가 정확히 그\n * 모양이었다 — 저쪽이 고쳐진 뒤 사람이 손으로 다시 넘겨서 나갔다.\n *\n * `again` 만 접는다. 끝없이 두드리는 것은 소음이고, 짧게 세 번 해서 안 되면 망이 흔들린 것이\n * 아니라 다른 것이다 — 그때는 사람이 봐야 한다.\n *\n * ── 멱등이 첫 시도와 재시도를 가른다 ────────────────────────────────────────\n * approved → 넘김 **첫 시도**다. 멱등 선언이 없어도 한다 — 한 번뿐이라 위험이 없다\n * failed → 넘김 **재시도**다. 두 번 보내도 지시가 하나라고 선언한 커넥터만 한다\n *\n * 선언하지 않은 커넥터의 실패는 목록에 남는다. 사람이 저쪽 상태를 보고 결정해야 한다 — 우리가\n * 「아마 괜찮겠지」로 두 번 보내면 현장에 지시가 두 건 선다.\n */\n\n/** 어댑터가 답하는 세 갈래. 비어 있으면 **말하지 않은 것**이고, `never` 가 아니다. */\nexport type RetryOutlook = 'again' | 'after-fix' | 'never'\n\nexport function isRetryOutlook(v: unknown): v is RetryOutlook {\n return v === 'again' || v === 'after-fix' || v === 'never'\n}\n\n/**\n * `again` 의 물러섬 — 30초 · 2분 · 10분, 그다음 접는다.\n *\n * 첫 값이 일꾼의 주기와 같다(§`RULE_PERIOD_MS`). 새 박자를 만들지 않는다.\n */\nexport const AGAIN_BACKOFF_MS = [30_000, 120_000, 600_000] as const\n\n/**\n * `after-fix` 는 **10분마다 계속** 본다 — 접지 않는다(머리말).\n *\n * 사람이 설정을 고치는 것을 기다리는 간격이다. 짧게 두면 헛되고, 길게 두면 고쳐 놓고 기다린다.\n */\nexport const AFTER_FIX_PERIOD_MS = 600_000\n\n/** 왜 집지 않는가 — 목록이 「고칠 것」과 「우리 쪽을 보지 말 것」을 갈라 보이게 한다. */\nexport type SkipReason =\n /** 종착이거나 넘길 수 없는 상태다. */\n | 'not-dispatchable'\n /** 아직 물러섬이 끝나지 않았다 — 다음 기회를 기다린다. */\n | 'backing-off'\n /** 어댑터가 「영원히 안 된다」고 답했다 — 사람이 **새 조치**를 내야 한다. 우리 쪽에 고칠 것이 없다. */\n | 'never'\n /** `again` 을 세 번 다 썼다 — 사람이 봐야 한다. */\n | 'attempts-spent'\n /** 어댑터가 다시 될 일인지 말하지 않았다 — 모르는 것을 재시도로 밀지 않는다. */\n | 'outlook-unknown'\n /** 재시도인데 커넥터가 멱등을 선언하지 않았다 — 두 건이 설 위험을 우리가 지지 않는다. */\n | 'not-idempotent'\n\nexport interface RetryDecision {\n /** 지금 넘길 것인가. */\n take: boolean\n /** 안 넘기면 왜 — 목록이 그대로 보인다. */\n skip?: SkipReason\n /** 넘길 것이면 이번이 몇 번째 시도인가(첫 시도가 1). */\n attempt?: number\n /** 이번 시도가 실패하면 다음은 언제인가(ISO). 없으면 더 안 본다. */\n nextAttemptAt?: string\n}\n\nexport interface RetryInput {\n /** 지금 상태 — `approved` 는 첫 시도, `failed` 는 재시도. */\n state: string\n /** 넘길 수 있는 상태인가 — 상태 기계가 답한다(`nextCommandState(state,'dispatch')`). */\n dispatchable: boolean\n /** 어댑터가 지난 실패에 답한 갈래. 첫 시도에는 없다. */\n retry?: string\n /** 지금까지 몇 번 넘겨 봤나. 없으면 0. */\n attempts?: number\n /** 다음에 볼 시각(ISO). 없으면 지금 봐도 된다. */\n nextAttemptAt?: string\n /** 이 커넥터가 두 번 보내도 지시가 하나인가 — **선언만 참으로 읽는다.** */\n idempotent?: boolean\n /** 지금(ms). */\n nowMs: number\n}\n\nconst at = (ms: number): string => new Date(ms).toISOString()\n\n/**\n * 이 조치를 지금 넘길 것인가.\n *\n * **모르면 넘기지 않는다.** 어댑터가 갈래를 말하지 않았거나 멱등을 선언하지 않았으면 재시도하지\n * 않고 목록에 남긴다 — 오늘 커널의 다른 축에서 지킨 것과 같은 규율이다(정격 사용량·품질 계수기·\n * 조용한 시각: 모르면 단언하지 않는다).\n */\nexport function retryDecisionOf(input: RetryInput): RetryDecision {\n if (!input.dispatchable) return { take: false, skip: 'not-dispatchable' }\n\n const attempts = typeof input.attempts === 'number' && input.attempts > 0 ? input.attempts : 0\n\n /*\n * **첫 시도** — 승인됐는데 한 번도 안 나간 것. 물러섬도 멱등도 묻지 않는다: 한 번뿐이라 두 건이\n * 설 수 없고, 어댑터는 아직 아무 답도 하지 않았다.\n *\n * 이 갈래가 승인 직후 발신이 놓친 것을 줍는다 — 커밋과 발신 사이에 프로세스가 죽은 경우다.\n */\n if (attempts === 0) {\n return { take: true, attempt: 1, nextAttemptAt: at(input.nowMs + AGAIN_BACKOFF_MS[0]) }\n }\n\n /* 재시도다. 여기서부터 어댑터의 답과 커넥터의 성질을 묻는다. */\n if (!isRetryOutlook(input.retry)) return { take: false, skip: 'outlook-unknown' }\n if (input.retry === 'never') return { take: false, skip: 'never' }\n if (input.idempotent !== true) return { take: false, skip: 'not-idempotent' }\n\n if (input.nextAttemptAt) {\n const dueMs = Date.parse(input.nextAttemptAt)\n /* 읽을 수 없는 값으로 막지 않는다 — 막으면 그 조치가 표에 갇힌다. */\n if (Number.isFinite(dueMs) && input.nowMs < dueMs) return { take: false, skip: 'backing-off' }\n }\n\n if (input.retry === 'after-fix') {\n /* 접지 않는다(머리말) — 같은 간격으로 계속 본다. */\n return { take: true, attempt: attempts + 1, nextAttemptAt: at(input.nowMs + AFTER_FIX_PERIOD_MS) }\n }\n\n /* `again` — 물러섬 표를 다 쓰면 접는다. */\n if (attempts >= AGAIN_BACKOFF_MS.length) return { take: false, skip: 'attempts-spent' }\n\n const next = AGAIN_BACKOFF_MS[attempts]\n return { take: true, attempt: attempts + 1, nextAttemptAt: at(input.nowMs + next) }\n}\n"]}
@@ -0,0 +1,127 @@
1
+ import type { CommandTypeSpec } from '@operato/ops-contract';
2
+ /** 어느 주의에 어느 지시를 붙이는가 — 선언 한 줄. */
3
+ export interface ActuationRuleSpec {
4
+ id: string;
5
+ /** 이 트윈에만 적용된다. 트윈마다 라인이 다르므로 전역 규칙은 두지 않는다. */
6
+ instanceId: string;
7
+ /** 어느 주의인가 — 커널의 안정 코드(`Attention.kind`). */
8
+ attentionKind: string;
9
+ /** 이 심각도 이상일 때만 — 없으면 종류만 본다. */
10
+ minSeverity?: 'low' | 'medium' | 'high' | 'critical';
11
+ /**
12
+ * **조건이 이만큼 이어졌을 때** 올린다.
13
+ *
14
+ * 없으면 처음 본 주기에 바로 올린다. 깜빡이는 조건에 현장 지시가 나가는 것을 막으려면 이 값이 필요하다
15
+ * — 커널이 조건이 처음 성립한 시각을 기억하므로(`Attention.timeStamp`) 여기서 잴 수 있다.
16
+ */
17
+ minHeldMs?: number;
18
+ /** 무엇을 내는가 — 선언된 조치 종류(`CommandTypeSpec.type`). 커널 자극이 아니다. */
19
+ commandType: string;
20
+ /**
21
+ * 지시의 내용 — `{이름}` 을 주의의 값으로 바꿔 넣는다.
22
+ *
23
+ * 바꿀 값은 `Attention.params` 와 `anchor` 에서 온다(`{locationId}` · `{ratioPct}`). 한 겹 중괄호를
24
+ * 쓰는 것은 이 저장소의 i18n 관례와 같다.
25
+ *
26
+ * 바꿀 것이 없으면 **그 자리를 비운다** — 지어낸 값을 넣지 않는다. 요구하는 칸이 비면 계약이 거절하고
27
+ * (§`missingCommandPayload`) 그 거절이 사람에게 전해진다.
28
+ */
29
+ payloadTemplate?: Record<string, unknown>;
30
+ /**
31
+ * **한 번 낸 근거에 다시 내기까지 기다리는 시간.**
32
+ *
33
+ * 이것이 없으면 승인하는 순간 규칙이 같은 것을 또 낸다 — 실제로 그랬다. 「결정을 기다리는 것」만 보고
34
+ * 중복을 막았는데, 승인하면 그 커맨드가 대기에서 빠지고 조건은 그대로 서 있기 때문이다. 사람이
35
+ * 승인할수록 대기 줄이 다시 차는 모양이 된다.
36
+ *
37
+ * 조건은 이어지는 것이고 조치는 시간이 걸린다. 낸 조치가 효과를 낼 때까지 기다리는 시간이 이 값이다.
38
+ */
39
+ cooldownMs?: number;
40
+ /** 꺼 둔 규칙은 판정하지 않는다. 지우지 않고 끌 수 있어야 한다. */
41
+ enabled?: boolean;
42
+ }
43
+ /** 판정에 들어가는 주의 — 커널이 낸 모양 그대로 중 필요한 것만. */
44
+ export interface AttentionLike {
45
+ id: string;
46
+ kind: string;
47
+ severity: string;
48
+ state?: string;
49
+ anchor?: {
50
+ locationId?: string;
51
+ moverId?: string;
52
+ orderId?: string;
53
+ operation?: string;
54
+ };
55
+ timeStamp?: string;
56
+ params?: Record<string, string | number>;
57
+ }
58
+ /** 올릴 것 하나. */
59
+ export interface RuleProposal {
60
+ ruleId: string;
61
+ attentionId: string;
62
+ attentionKind: string;
63
+ commandType: string;
64
+ payload: Record<string, unknown>;
65
+ }
66
+ /** 올리지 않은 것과 그 이유 — **말하지 않으면 사람은 규칙이 도는 줄 안다.** */
67
+ export interface RuleSkip {
68
+ ruleId: string;
69
+ attentionId?: string;
70
+ reason: string;
71
+ }
72
+ export interface RuleVerdict {
73
+ proposals: RuleProposal[];
74
+ /** 왜 안 올렸나. 조용히 넘어가지 않는다. */
75
+ skips: RuleSkip[];
76
+ /** 상한에 닿아 잘랐나 — 잘랐으면 몇 개를 두고 왔나. */
77
+ withheld?: number;
78
+ }
79
+ /**
80
+ * 이번에 무엇을 올릴지 정한다 — **순수 함수**. 저장소도 시계도 보지 않는다.
81
+ *
82
+ * 시험이 재기동과 상한과 중복을 다 걸을 수 있어야 하고, 그러려면 이 판정이 밖에서 값을 받아야 한다.
83
+ * 이 저장소에서 가장 자주 나는 결함이 「자리는 있고 길이 없다」이고, 게이트에 물리는 것이므로 여기서
84
+ * 나면 결과가 현장의 지시다.
85
+ */
86
+ export declare function evaluateRules(args: {
87
+ rules: readonly ActuationRuleSpec[];
88
+ attentions: readonly AttentionLike[];
89
+ /** 선언된 조치 종류 — 없는 종류를 가리키는 규칙은 여기서 걸린다. */
90
+ specs: readonly CommandTypeSpec[];
91
+ /**
92
+ * **이미 다루고 있는 근거들** — 근거마다 마지막으로 조치를 낸 시각.
93
+ *
94
+ * 결정을 기다리는 것만 보면 부족하다. 승인하는 순간 그 커맨드가 대기에서 빠지고 조건은 그대로
95
+ * 서 있어서, 다음 바퀴가 같은 것을 또 낸다 — 걸어 보고 그 상태를 실제로 만들었다.
96
+ *
97
+ * 그래서 **낸 시각**을 받는다. 결정을 기다리는 것은 시각과 무관하게 막는다(§`openCauses`).
98
+ * 저장소에서 읽어 넘긴다 — 프로세스 기억에 두면 재기동이 곧 중복이 된다.
99
+ */
100
+ openCauses: ReadonlySet<string>;
101
+ /** 근거 → 마지막으로 조치를 낸 시각(ms). 없으면 낸 적이 없다. */
102
+ lastProposedAt?: ReadonlyMap<string, number>;
103
+ /** 지금 — 조건이 얼마나 이어졌는지 재는 기준. */
104
+ nowMs: number;
105
+ /** 한 번에 몇 개까지 올리나. 넘는 것은 두고 오고, 두고 온 수를 말한다. */
106
+ maxPerRun?: number;
107
+ }): RuleVerdict;
108
+ /** 한 번에 올리는 상한 — 사람이 읽을 수 있는 수. 읽지 않는 대기 줄은 게이트가 아니다. */
109
+ export declare const DEFAULT_MAX_PER_RUN = 20;
110
+ /**
111
+ * 같은 근거에 다시 내기까지 — **선언하지 않아도 기다린다.**
112
+ *
113
+ * 기본을 0 으로 두면 선언하지 않은 규칙이 승인할 때마다 같은 것을 또 낸다. 그 결함을 기본값으로 두지
114
+ * 않는다. 더 빨리 내야 하는 규칙은 스스로 짧게 적는다.
115
+ */
116
+ export declare const DEFAULT_COOLDOWN_MS: number;
117
+ /**
118
+ * 이 규칙이 낼 수 있는 종류인가 — **판정이 한 자리에만 있게.**
119
+ *
120
+ * 저장할 때와 돌 때가 같은 것을 봐야 한다. 두 벌이면 저장은 되는데 안 도는 규칙이 생기고, 사람은 규칙이
121
+ * 걸린 줄로 읽는다.
122
+ *
123
+ * `stimuli` 를 받는 이유: 커널 자극(`CMD`)을 적었을 때 **왜 안 되는지**를 말하려면 그 어휘를 알아야 한다.
124
+ * 「선언되지 않았다」로만 답하면 부르는 쪽은 선언이 빠진 줄로 읽고 선언을 추가한다 — 그 순간 모델 안의
125
+ * 자극이 현장 지시가 된다.
126
+ */
127
+ export declare function commandTypeGap(commandType: string, specs: readonly CommandTypeSpec[], stimuli?: readonly string[]): string | undefined;
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_COOLDOWN_MS = exports.DEFAULT_MAX_PER_RUN = void 0;
4
+ exports.evaluateRules = evaluateRules;
5
+ exports.commandTypeGap = commandTypeGap;
6
+ const SEVERITY_ORDER = ['low', 'medium', 'high', 'critical'];
7
+ /** 심각도가 문턱에 닿나 — 모르는 값은 닿지 않는 것으로 본다(모름을 통과로 읽지 않는다). */
8
+ function severityMeets(actual, min) {
9
+ if (!min)
10
+ return true;
11
+ const a = SEVERITY_ORDER.indexOf(actual);
12
+ const m = SEVERITY_ORDER.indexOf(min);
13
+ if (a < 0 || m < 0)
14
+ return false;
15
+ return a >= m;
16
+ }
17
+ /**
18
+ * `{이름}` 을 바꿔 넣는다 — 바꿀 값이 없으면 **그 칸을 뺀다.**
19
+ *
20
+ * 원래 문자열을 그대로 두면 `{quantity}` 같은 글자가 지시의 수량으로 들어가고, 그 지시가 현장에 나간다.
21
+ * 빼면 계약이 「요구하는 칸이 없다」로 거절하고 그 거절이 사람에게 전해진다.
22
+ */
23
+ function fill(template, values) {
24
+ const payload = {};
25
+ const missing = [];
26
+ if (!template)
27
+ return { payload, missing };
28
+ for (const [key, raw] of Object.entries(template)) {
29
+ if (typeof raw !== 'string') {
30
+ payload[key] = raw;
31
+ continue;
32
+ }
33
+ /* 값 하나를 그대로 나르는 자리 — 수를 문자열로 만들지 않는다(수량이 문자열이면 계산이 무너진다). */
34
+ const whole = /^\{([A-Za-z0-9_.]+)\}$/.exec(raw);
35
+ if (whole) {
36
+ const name = whole[1];
37
+ if (!(name in values)) {
38
+ missing.push(name);
39
+ continue;
40
+ }
41
+ payload[key] = values[name];
42
+ continue;
43
+ }
44
+ let gap = false;
45
+ const text = raw.replace(/\{([A-Za-z0-9_.]+)\}/g, (_m, name) => {
46
+ if (name in values)
47
+ return String(values[name]);
48
+ missing.push(name);
49
+ gap = true;
50
+ return '';
51
+ });
52
+ if (!gap)
53
+ payload[key] = text;
54
+ }
55
+ return { payload, missing };
56
+ }
57
+ /** 바꿔 넣을 값 — 주의의 `params` 와 `anchor` 에서. */
58
+ function valuesOf(attention) {
59
+ const out = { ...(attention.params ?? {}) };
60
+ for (const [key, value] of Object.entries(attention.anchor ?? {})) {
61
+ if (value !== undefined && value !== null && value !== '')
62
+ out[key] = value;
63
+ }
64
+ out.attentionId = attention.id;
65
+ out.attentionKind = attention.kind;
66
+ return out;
67
+ }
68
+ /**
69
+ * 이번에 무엇을 올릴지 정한다 — **순수 함수**. 저장소도 시계도 보지 않는다.
70
+ *
71
+ * 시험이 재기동과 상한과 중복을 다 걸을 수 있어야 하고, 그러려면 이 판정이 밖에서 값을 받아야 한다.
72
+ * 이 저장소에서 가장 자주 나는 결함이 「자리는 있고 길이 없다」이고, 게이트에 물리는 것이므로 여기서
73
+ * 나면 결과가 현장의 지시다.
74
+ */
75
+ function evaluateRules(args) {
76
+ const { rules, attentions, specs, openCauses, nowMs } = args;
77
+ const maxPerRun = args.maxPerRun ?? exports.DEFAULT_MAX_PER_RUN;
78
+ const proposals = [];
79
+ const skips = [];
80
+ let withheld = 0;
81
+ /* 한 주의에 규칙이 둘 붙어 있어도 조치는 하나만 올린다 — 같은 조건에 두 지시가 나가면 현장이 두 번 움직인다. */
82
+ const claimed = new Set(openCauses);
83
+ for (const rule of rules) {
84
+ if (rule.enabled === false)
85
+ continue;
86
+ const gap = commandTypeGap(rule.commandType, specs);
87
+ if (gap) {
88
+ skips.push({ ruleId: rule.id, reason: gap });
89
+ continue;
90
+ }
91
+ const spec = specs.find(s => s.type === rule.commandType);
92
+ for (const attention of attentions) {
93
+ if (attention.kind !== rule.attentionKind)
94
+ continue;
95
+ /* 확인된 것과 사라진 것에는 올리지 않는다 — 사람이 이미 본 것이다. */
96
+ if (attention.state && attention.state !== 'active')
97
+ continue;
98
+ if (!severityMeets(attention.severity, rule.minSeverity))
99
+ continue;
100
+ if (claimed.has(attention.id))
101
+ continue;
102
+ /*
103
+ * 낸 지 얼마 안 됐으면 기다린다. 조건은 이어지는 것이고 조치는 시간이 걸린다 — 효과를 보기 전에
104
+ * 또 내면 대기 줄이 같은 지시로 찬다.
105
+ */
106
+ const lastAt = args.lastProposedAt?.get(attention.id);
107
+ const cooldown = rule.cooldownMs ?? exports.DEFAULT_COOLDOWN_MS;
108
+ if (lastAt !== undefined && cooldown > 0 && nowMs - lastAt < cooldown)
109
+ continue;
110
+ const held = heldMs(attention, nowMs);
111
+ if (rule.minHeldMs && rule.minHeldMs > 0) {
112
+ if (held === undefined) {
113
+ /*
114
+ * 언제부터인지 모르는 주의다. **모름을 「충분히 오래됐다」로 읽지 않는다** — 그렇게 읽으면
115
+ * 방금 생긴 깜빡임에 현장 지시가 나간다.
116
+ */
117
+ skips.push({
118
+ ruleId: rule.id,
119
+ attentionId: attention.id,
120
+ reason: `이 주의는 언제부터인지 말하지 않는다 — ${rule.minHeldMs}ms 이어졌는지 알 수 없어 올리지 않았다`
121
+ });
122
+ continue;
123
+ }
124
+ if (held < rule.minHeldMs)
125
+ continue;
126
+ }
127
+ if (proposals.length >= maxPerRun) {
128
+ withheld++;
129
+ continue;
130
+ }
131
+ const { payload, missing } = fill(rule.payloadTemplate, valuesOf(attention));
132
+ if (missing.length) {
133
+ skips.push({
134
+ ruleId: rule.id,
135
+ attentionId: attention.id,
136
+ reason: `지시에 넣을 값이 주의에 없다 (${[...new Set(missing)].join(', ')}) — 지어낸 값으로 현장 지시를 만들지 않았다`
137
+ });
138
+ continue;
139
+ }
140
+ const gaps = missingRequired(spec, payload);
141
+ if (gaps.length) {
142
+ skips.push({
143
+ ruleId: rule.id,
144
+ attentionId: attention.id,
145
+ reason: `지시가 요구하는 칸이 비었다 (${gaps.join(', ')}) — 무엇을 지시하는지 모르면 승인할 근거가 없다`
146
+ });
147
+ continue;
148
+ }
149
+ claimed.add(attention.id);
150
+ proposals.push({
151
+ ruleId: rule.id,
152
+ attentionId: attention.id,
153
+ attentionKind: attention.kind,
154
+ commandType: rule.commandType,
155
+ payload
156
+ });
157
+ }
158
+ }
159
+ return { proposals, skips, ...(withheld ? { withheld } : {}) };
160
+ }
161
+ /** 한 번에 올리는 상한 — 사람이 읽을 수 있는 수. 읽지 않는 대기 줄은 게이트가 아니다. */
162
+ exports.DEFAULT_MAX_PER_RUN = 20;
163
+ /**
164
+ * 같은 근거에 다시 내기까지 — **선언하지 않아도 기다린다.**
165
+ *
166
+ * 기본을 0 으로 두면 선언하지 않은 규칙이 승인할 때마다 같은 것을 또 낸다. 그 결함을 기본값으로 두지
167
+ * 않는다. 더 빨리 내야 하는 규칙은 스스로 짧게 적는다.
168
+ */
169
+ exports.DEFAULT_COOLDOWN_MS = 60 * 60_000;
170
+ /**
171
+ * 이 규칙이 낼 수 있는 종류인가 — **판정이 한 자리에만 있게.**
172
+ *
173
+ * 저장할 때와 돌 때가 같은 것을 봐야 한다. 두 벌이면 저장은 되는데 안 도는 규칙이 생기고, 사람은 규칙이
174
+ * 걸린 줄로 읽는다.
175
+ *
176
+ * `stimuli` 를 받는 이유: 커널 자극(`CMD`)을 적었을 때 **왜 안 되는지**를 말하려면 그 어휘를 알아야 한다.
177
+ * 「선언되지 않았다」로만 답하면 부르는 쪽은 선언이 빠진 줄로 읽고 선언을 추가한다 — 그 순간 모델 안의
178
+ * 자극이 현장 지시가 된다.
179
+ */
180
+ function commandTypeGap(commandType, specs, stimuli) {
181
+ if (specs.some(s => s.type === commandType))
182
+ return undefined;
183
+ if (stimuli?.includes(commandType)) {
184
+ return (`그것은 커널 안에서 도는 자극이다 (${commandType}) — 현장에 나가는 지시가 아니다. ` +
185
+ '모델 상태를 바꾸려면 커널에 자극을 넣고, 현장에 무언가를 시키려면 그 지시를 조치 종류로 선언한다');
186
+ }
187
+ return `선언되지 않은 조치 종류를 가리킨다 (${commandType}) — 선언된 것: ${specs.map(s => s.type).join(', ') || '(없음)'}`;
188
+ }
189
+ /** 조건이 얼마나 이어졌나 — 모르면 `undefined`(0 이 아니다). */
190
+ function heldMs(attention, nowMs) {
191
+ if (!attention.timeStamp)
192
+ return undefined;
193
+ const at = Date.parse(attention.timeStamp);
194
+ if (!Number.isFinite(at))
195
+ return undefined;
196
+ return Math.max(0, nowMs - at);
197
+ }
198
+ /** 계약의 요구를 그대로 본다 — 판정을 여기서 다시 쓰지 않는다. */
199
+ function missingRequired(spec, payload) {
200
+ return (spec.required ?? []).filter(key => {
201
+ const v = payload[key];
202
+ return v === undefined || v === null || v === '';
203
+ });
204
+ }
205
+ //# sourceMappingURL=rule-evaluate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-evaluate.js","sourceRoot":"","sources":["../../../server/service/actuation/rule-evaluate.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **관측에서 조치를 낸다** — 그 판정 하나.\n *\n * ── 무엇이 없었나 (2026-09-01) ───────────────────────────────────────────\n * 승인 게이트를 세우고 나니 **낼 수 있는 것이 사람뿐**이었다. 계약은 낸 쪽을 네 가지로 두는데\n * (`operator` · `rule` · `ai` · `schedule`) 사람 말고는 길이 없었다. 게이트의 값어치는 트윈이 스스로\n * 알아챈 것을 사람이 판단하는 데 있는데, 알아챈 것을 올릴 자리가 없었다.\n *\n * ── 조건을 새로 만들지 않는다 ────────────────────────────────────────────\n * 커널이 이미 상태에서 「봐야 할 것」을 판단해 `Attention` 으로 낸다 — 안정된 종류 코드(`kind`),\n * 무엇에 대한 것인지(`anchor`), 언제부터인지(`timeStamp`), 표현용 값(`params`). 조건 언어를 새로 만들면\n * 같은 판단이 두 벌이 되고, 한쪽만 고쳐지는 날이 온다.\n *\n * 그래서 규칙은 **어느 주의에 어느 지시를 붙이는가**만 정한다.\n *\n * ── 자극과 지시를 잇지 않는다 ────────────────────────────────────────────\n * 주의에는 `suggestedAction.command` 가 실려 있지만 그것은 **커널 안에서 도는 자극**이다\n * (`resource.repair` = 모델에서 수리된 것으로 둔다). 여기서 내는 것은 현장에 나가는 지시다. 그 값을\n * 그대로 쓰면 「모델을 그렇게 두라」가 「사람을 보내라」가 된다(§`propose` 의 거절).\n *\n * ── 무너지는 것 넷 ───────────────────────────────────────────────────────\n * 행복한 길은 짧다. 실제로 무너지는 것은 이것들이다.\n *\n * 같은 조건이 매 주기 다시 올라온다 대기 줄에 같은 지시가 수십 개 선다\n * 방금 생긴 조건에 바로 올린다 깜빡인 것에 현장 지시가 나간다\n * 조건이 사라진 뒤에도 대기에 남는다 승인하는 사람이 없는 문제를 판단한다\n * 한 번에 수백 개가 올라온다 사람이 읽을 수 없고, 읽지 않으면 게이트가 시늉이 된다\n *\n * 넷 다 오류가 나지 않는다. 그래서 이 파일이 넷을 다룬다.\n */\nimport type { CommandTypeSpec } from '@operato/ops-contract'\n\n/** 어느 주의에 어느 지시를 붙이는가 — 선언 한 줄. */\nexport interface ActuationRuleSpec {\n id: string\n /** 이 트윈에만 적용된다. 트윈마다 라인이 다르므로 전역 규칙은 두지 않는다. */\n instanceId: string\n /** 어느 주의인가 — 커널의 안정 코드(`Attention.kind`). */\n attentionKind: string\n /** 이 심각도 이상일 때만 — 없으면 종류만 본다. */\n minSeverity?: 'low' | 'medium' | 'high' | 'critical'\n /**\n * **조건이 이만큼 이어졌을 때** 올린다.\n *\n * 없으면 처음 본 주기에 바로 올린다. 깜빡이는 조건에 현장 지시가 나가는 것을 막으려면 이 값이 필요하다\n * — 커널이 조건이 처음 성립한 시각을 기억하므로(`Attention.timeStamp`) 여기서 잴 수 있다.\n */\n minHeldMs?: number\n /** 무엇을 내는가 — 선언된 조치 종류(`CommandTypeSpec.type`). 커널 자극이 아니다. */\n commandType: string\n /**\n * 지시의 내용 — `{이름}` 을 주의의 값으로 바꿔 넣는다.\n *\n * 바꿀 값은 `Attention.params` 와 `anchor` 에서 온다(`{locationId}` · `{ratioPct}`). 한 겹 중괄호를\n * 쓰는 것은 이 저장소의 i18n 관례와 같다.\n *\n * 바꿀 것이 없으면 **그 자리를 비운다** — 지어낸 값을 넣지 않는다. 요구하는 칸이 비면 계약이 거절하고\n * (§`missingCommandPayload`) 그 거절이 사람에게 전해진다.\n */\n payloadTemplate?: Record<string, unknown>\n /**\n * **한 번 낸 근거에 다시 내기까지 기다리는 시간.**\n *\n * 이것이 없으면 승인하는 순간 규칙이 같은 것을 또 낸다 — 실제로 그랬다. 「결정을 기다리는 것」만 보고\n * 중복을 막았는데, 승인하면 그 커맨드가 대기에서 빠지고 조건은 그대로 서 있기 때문이다. 사람이\n * 승인할수록 대기 줄이 다시 차는 모양이 된다.\n *\n * 조건은 이어지는 것이고 조치는 시간이 걸린다. 낸 조치가 효과를 낼 때까지 기다리는 시간이 이 값이다.\n */\n cooldownMs?: number\n /** 꺼 둔 규칙은 판정하지 않는다. 지우지 않고 끌 수 있어야 한다. */\n enabled?: boolean\n}\n\n/** 판정에 들어가는 주의 — 커널이 낸 모양 그대로 중 필요한 것만. */\nexport interface AttentionLike {\n id: string\n kind: string\n severity: string\n state?: string\n anchor?: { locationId?: string; moverId?: string; orderId?: string; operation?: string }\n timeStamp?: string\n params?: Record<string, string | number>\n}\n\n/** 올릴 것 하나. */\nexport interface RuleProposal {\n ruleId: string\n attentionId: string\n attentionKind: string\n commandType: string\n payload: Record<string, unknown>\n}\n\n/** 올리지 않은 것과 그 이유 — **말하지 않으면 사람은 규칙이 도는 줄 안다.** */\nexport interface RuleSkip {\n ruleId: string\n attentionId?: string\n reason: string\n}\n\nexport interface RuleVerdict {\n proposals: RuleProposal[]\n /** 왜 안 올렸나. 조용히 넘어가지 않는다. */\n skips: RuleSkip[]\n /** 상한에 닿아 잘랐나 — 잘랐으면 몇 개를 두고 왔나. */\n withheld?: number\n}\n\nconst SEVERITY_ORDER = ['low', 'medium', 'high', 'critical'] as const\n\n/** 심각도가 문턱에 닿나 — 모르는 값은 닿지 않는 것으로 본다(모름을 통과로 읽지 않는다). */\nfunction severityMeets(actual: string, min: string | undefined): boolean {\n if (!min) return true\n const a = SEVERITY_ORDER.indexOf(actual as any)\n const m = SEVERITY_ORDER.indexOf(min as any)\n if (a < 0 || m < 0) return false\n return a >= m\n}\n\n/**\n * `{이름}` 을 바꿔 넣는다 — 바꿀 값이 없으면 **그 칸을 뺀다.**\n *\n * 원래 문자열을 그대로 두면 `{quantity}` 같은 글자가 지시의 수량으로 들어가고, 그 지시가 현장에 나간다.\n * 빼면 계약이 「요구하는 칸이 없다」로 거절하고 그 거절이 사람에게 전해진다.\n */\nfunction fill(template: Record<string, unknown> | undefined, values: Record<string, string | number>): {\n payload: Record<string, unknown>\n missing: string[]\n} {\n const payload: Record<string, unknown> = {}\n const missing: string[] = []\n if (!template) return { payload, missing }\n\n for (const [key, raw] of Object.entries(template)) {\n if (typeof raw !== 'string') {\n payload[key] = raw\n continue\n }\n\n /* 값 하나를 그대로 나르는 자리 — 수를 문자열로 만들지 않는다(수량이 문자열이면 계산이 무너진다). */\n const whole = /^\\{([A-Za-z0-9_.]+)\\}$/.exec(raw)\n if (whole) {\n const name = whole[1]!\n if (!(name in values)) {\n missing.push(name)\n continue\n }\n payload[key] = values[name]\n continue\n }\n\n let gap = false\n const text = raw.replace(/\\{([A-Za-z0-9_.]+)\\}/g, (_m, name: string) => {\n if (name in values) return String(values[name])\n missing.push(name)\n gap = true\n return ''\n })\n if (!gap) payload[key] = text\n }\n\n return { payload, missing }\n}\n\n/** 바꿔 넣을 값 — 주의의 `params` 와 `anchor` 에서. */\nfunction valuesOf(attention: AttentionLike): Record<string, string | number> {\n const out: Record<string, string | number> = { ...(attention.params ?? {}) }\n for (const [key, value] of Object.entries(attention.anchor ?? {})) {\n if (value !== undefined && value !== null && value !== '') out[key] = value as string\n }\n out.attentionId = attention.id\n out.attentionKind = attention.kind\n return out\n}\n\n/**\n * 이번에 무엇을 올릴지 정한다 — **순수 함수**. 저장소도 시계도 보지 않는다.\n *\n * 시험이 재기동과 상한과 중복을 다 걸을 수 있어야 하고, 그러려면 이 판정이 밖에서 값을 받아야 한다.\n * 이 저장소에서 가장 자주 나는 결함이 「자리는 있고 길이 없다」이고, 게이트에 물리는 것이므로 여기서\n * 나면 결과가 현장의 지시다.\n */\nexport function evaluateRules(args: {\n rules: readonly ActuationRuleSpec[]\n attentions: readonly AttentionLike[]\n /** 선언된 조치 종류 — 없는 종류를 가리키는 규칙은 여기서 걸린다. */\n specs: readonly CommandTypeSpec[]\n /**\n * **이미 다루고 있는 근거들** — 근거마다 마지막으로 조치를 낸 시각.\n *\n * 결정을 기다리는 것만 보면 부족하다. 승인하는 순간 그 커맨드가 대기에서 빠지고 조건은 그대로\n * 서 있어서, 다음 바퀴가 같은 것을 또 낸다 — 걸어 보고 그 상태를 실제로 만들었다.\n *\n * 그래서 **낸 시각**을 받는다. 결정을 기다리는 것은 시각과 무관하게 막는다(§`openCauses`).\n * 저장소에서 읽어 넘긴다 — 프로세스 기억에 두면 재기동이 곧 중복이 된다.\n */\n openCauses: ReadonlySet<string>\n /** 근거 → 마지막으로 조치를 낸 시각(ms). 없으면 낸 적이 없다. */\n lastProposedAt?: ReadonlyMap<string, number>\n /** 지금 — 조건이 얼마나 이어졌는지 재는 기준. */\n nowMs: number\n /** 한 번에 몇 개까지 올리나. 넘는 것은 두고 오고, 두고 온 수를 말한다. */\n maxPerRun?: number\n}): RuleVerdict {\n const { rules, attentions, specs, openCauses, nowMs } = args\n const maxPerRun = args.maxPerRun ?? DEFAULT_MAX_PER_RUN\n\n const proposals: RuleProposal[] = []\n const skips: RuleSkip[] = []\n let withheld = 0\n\n /* 한 주의에 규칙이 둘 붙어 있어도 조치는 하나만 올린다 — 같은 조건에 두 지시가 나가면 현장이 두 번 움직인다. */\n const claimed = new Set<string>(openCauses)\n\n for (const rule of rules) {\n if (rule.enabled === false) continue\n\n const gap = commandTypeGap(rule.commandType, specs)\n if (gap) {\n skips.push({ ruleId: rule.id, reason: gap })\n continue\n }\n const spec = specs.find(s => s.type === rule.commandType)!\n\n for (const attention of attentions) {\n if (attention.kind !== rule.attentionKind) continue\n\n /* 확인된 것과 사라진 것에는 올리지 않는다 — 사람이 이미 본 것이다. */\n if (attention.state && attention.state !== 'active') continue\n\n if (!severityMeets(attention.severity, rule.minSeverity)) continue\n\n if (claimed.has(attention.id)) continue\n\n /*\n * 낸 지 얼마 안 됐으면 기다린다. 조건은 이어지는 것이고 조치는 시간이 걸린다 — 효과를 보기 전에\n * 또 내면 대기 줄이 같은 지시로 찬다.\n */\n const lastAt = args.lastProposedAt?.get(attention.id)\n const cooldown = rule.cooldownMs ?? DEFAULT_COOLDOWN_MS\n if (lastAt !== undefined && cooldown > 0 && nowMs - lastAt < cooldown) continue\n\n const held = heldMs(attention, nowMs)\n if (rule.minHeldMs && rule.minHeldMs > 0) {\n if (held === undefined) {\n /*\n * 언제부터인지 모르는 주의다. **모름을 「충분히 오래됐다」로 읽지 않는다** — 그렇게 읽으면\n * 방금 생긴 깜빡임에 현장 지시가 나간다.\n */\n skips.push({\n ruleId: rule.id,\n attentionId: attention.id,\n reason: `이 주의는 언제부터인지 말하지 않는다 — ${rule.minHeldMs}ms 이어졌는지 알 수 없어 올리지 않았다`\n })\n continue\n }\n if (held < rule.minHeldMs) continue\n }\n\n if (proposals.length >= maxPerRun) {\n withheld++\n continue\n }\n\n const { payload, missing } = fill(rule.payloadTemplate, valuesOf(attention))\n if (missing.length) {\n skips.push({\n ruleId: rule.id,\n attentionId: attention.id,\n reason: `지시에 넣을 값이 주의에 없다 (${[...new Set(missing)].join(', ')}) — 지어낸 값으로 현장 지시를 만들지 않았다`\n })\n continue\n }\n\n const gaps = missingRequired(spec, payload)\n if (gaps.length) {\n skips.push({\n ruleId: rule.id,\n attentionId: attention.id,\n reason: `지시가 요구하는 칸이 비었다 (${gaps.join(', ')}) — 무엇을 지시하는지 모르면 승인할 근거가 없다`\n })\n continue\n }\n\n claimed.add(attention.id)\n proposals.push({\n ruleId: rule.id,\n attentionId: attention.id,\n attentionKind: attention.kind,\n commandType: rule.commandType,\n payload\n })\n }\n }\n\n return { proposals, skips, ...(withheld ? { withheld } : {}) }\n}\n\n/** 한 번에 올리는 상한 — 사람이 읽을 수 있는 수. 읽지 않는 대기 줄은 게이트가 아니다. */\nexport const DEFAULT_MAX_PER_RUN = 20\n\n/**\n * 같은 근거에 다시 내기까지 — **선언하지 않아도 기다린다.**\n *\n * 기본을 0 으로 두면 선언하지 않은 규칙이 승인할 때마다 같은 것을 또 낸다. 그 결함을 기본값으로 두지\n * 않는다. 더 빨리 내야 하는 규칙은 스스로 짧게 적는다.\n */\nexport const DEFAULT_COOLDOWN_MS = 60 * 60_000\n\n/**\n * 이 규칙이 낼 수 있는 종류인가 — **판정이 한 자리에만 있게.**\n *\n * 저장할 때와 돌 때가 같은 것을 봐야 한다. 두 벌이면 저장은 되는데 안 도는 규칙이 생기고, 사람은 규칙이\n * 걸린 줄로 읽는다.\n *\n * `stimuli` 를 받는 이유: 커널 자극(`CMD`)을 적었을 때 **왜 안 되는지**를 말하려면 그 어휘를 알아야 한다.\n * 「선언되지 않았다」로만 답하면 부르는 쪽은 선언이 빠진 줄로 읽고 선언을 추가한다 — 그 순간 모델 안의\n * 자극이 현장 지시가 된다.\n */\nexport function commandTypeGap(\n commandType: string,\n specs: readonly CommandTypeSpec[],\n stimuli?: readonly string[]\n): string | undefined {\n if (specs.some(s => s.type === commandType)) return undefined\n\n if (stimuli?.includes(commandType)) {\n return (\n `그것은 커널 안에서 도는 자극이다 (${commandType}) — 현장에 나가는 지시가 아니다. ` +\n '모델 상태를 바꾸려면 커널에 자극을 넣고, 현장에 무언가를 시키려면 그 지시를 조치 종류로 선언한다'\n )\n }\n\n return `선언되지 않은 조치 종류를 가리킨다 (${commandType}) — 선언된 것: ${specs.map(s => s.type).join(', ') || '(없음)'}`\n}\n\n/** 조건이 얼마나 이어졌나 — 모르면 `undefined`(0 이 아니다). */\nfunction heldMs(attention: AttentionLike, nowMs: number): number | undefined {\n if (!attention.timeStamp) return undefined\n const at = Date.parse(attention.timeStamp)\n if (!Number.isFinite(at)) return undefined\n return Math.max(0, nowMs - at)\n}\n\n/** 계약의 요구를 그대로 본다 — 판정을 여기서 다시 쓰지 않는다. */\nfunction missingRequired(spec: CommandTypeSpec, payload: Record<string, unknown>): string[] {\n return (spec.required ?? []).filter(key => {\n const v = payload[key]\n return v === undefined || v === null || v === ''\n })\n}\n"]}
@@ -0,0 +1,5 @@
1
+ /** 몇 초마다 도나. 오래 이어진 조건을 보는 일이라 촘촘할 이유가 없다. */
2
+ export declare const RULE_PERIOD_MS = 30000;
3
+ /** 주기를 세운다 — 두 번 불러도 하나만 선다. */
4
+ export declare function startActuationRuleLoop(periodMs?: number): void;
5
+ export declare function stopActuationRuleLoop(): void;