@things-factory/headless-twin 10.0.18 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +10 -1
  2. package/dist-server/engine/canonical-ingest.js +110 -33
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/event-time.d.ts +25 -0
  5. package/dist-server/engine/event-time.js +98 -0
  6. package/dist-server/engine/event-time.js.map +1 -0
  7. package/dist-server/engine/index.d.ts +2 -0
  8. package/dist-server/engine/index.js +23 -0
  9. package/dist-server/engine/index.js.map +1 -1
  10. package/dist-server/engine/ingest-dedupe.d.ts +18 -6
  11. package/dist-server/engine/ingest-dedupe.js +201 -15
  12. package/dist-server/engine/ingest-dedupe.js.map +1 -1
  13. package/dist-server/engine/ingest-health.d.ts +60 -1
  14. package/dist-server/engine/ingest-health.js +43 -8
  15. package/dist-server/engine/ingest-health.js.map +1 -1
  16. package/dist-server/engine/kpi-fold.d.ts +19 -0
  17. package/dist-server/engine/kpi-fold.js +4 -1
  18. package/dist-server/engine/kpi-fold.js.map +1 -1
  19. package/dist-server/engine/kpi-query.js +23 -3
  20. package/dist-server/engine/kpi-query.js.map +1 -1
  21. package/dist-server/engine/oee-accumulator.d.ts +35 -2
  22. package/dist-server/engine/oee-accumulator.js +56 -4
  23. package/dist-server/engine/oee-accumulator.js.map +1 -1
  24. package/dist-server/engine/twin-engine.d.ts +72 -3
  25. package/dist-server/engine/twin-engine.js +118 -5
  26. package/dist-server/engine/twin-engine.js.map +1 -1
  27. package/dist-server/index.js +21 -0
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
  30. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
  31. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
  32. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
  33. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
  34. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
  35. package/dist-server/migrations/index.js +5 -1
  36. package/dist-server/migrations/index.js.map +1 -1
  37. package/dist-server/routes.js +19 -16
  38. package/dist-server/routes.js.map +1 -1
  39. package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
  40. package/dist-server/service/actuation/actuation-advisor.js +53 -0
  41. package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
  42. package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
  43. package/dist-server/service/actuation/actuation-outcome.js +34 -0
  44. package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
  45. package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
  46. package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
  47. package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
  48. package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
  49. package/dist-server/service/actuation/actuation-rule.js +130 -0
  50. package/dist-server/service/actuation/actuation-rule.js.map +1 -0
  51. package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
  52. package/dist-server/service/actuation/approval-gateway.js +14 -0
  53. package/dist-server/service/actuation/approval-gateway.js.map +1 -0
  54. package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
  55. package/dist-server/service/actuation/catch-up-effects.js +120 -0
  56. package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
  57. package/dist-server/service/actuation/command-dispatcher.d.ts +230 -0
  58. package/dist-server/service/actuation/command-dispatcher.js +269 -0
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
  60. package/dist-server/service/actuation/command-store.d.ts +11 -0
  61. package/dist-server/service/actuation/command-store.js +219 -0
  62. package/dist-server/service/actuation/command-store.js.map +1 -0
  63. package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
  64. package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
  65. package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
  66. package/dist-server/service/actuation/effect-match.d.ts +53 -0
  67. package/dist-server/service/actuation/effect-match.js +65 -0
  68. package/dist-server/service/actuation/effect-match.js.map +1 -0
  69. package/dist-server/service/actuation/index.d.ts +25 -0
  70. package/dist-server/service/actuation/index.js +30 -0
  71. package/dist-server/service/actuation/index.js.map +1 -0
  72. package/dist-server/service/actuation/retry-plan.d.ts +63 -0
  73. package/dist-server/service/actuation/retry-plan.js +99 -0
  74. package/dist-server/service/actuation/retry-plan.js.map +1 -0
  75. package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
  76. package/dist-server/service/actuation/rule-evaluate.js +205 -0
  77. package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
  78. package/dist-server/service/actuation/rule-loop.d.ts +5 -0
  79. package/dist-server/service/actuation/rule-loop.js +156 -0
  80. package/dist-server/service/actuation/rule-loop.js.map +1 -0
  81. package/dist-server/service/actuation/rule-runner.d.ts +52 -0
  82. package/dist-server/service/actuation/rule-runner.js +135 -0
  83. package/dist-server/service/actuation/rule-runner.js.map +1 -0
  84. package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
  85. package/dist-server/service/actuation/rule-wiring.js +80 -0
  86. package/dist-server/service/actuation/rule-wiring.js.map +1 -0
  87. package/dist-server/service/actuation/settle-effects.d.ts +21 -0
  88. package/dist-server/service/actuation/settle-effects.js +101 -0
  89. package/dist-server/service/actuation/settle-effects.js.map +1 -0
  90. package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
  91. package/dist-server/service/actuation/sweep-dispatch.js +125 -0
  92. package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
  93. package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
  94. package/dist-server/service/actuation/twin-command-resolver.js +406 -0
  95. package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
  96. package/dist-server/service/actuation/twin-command.d.ts +111 -0
  97. package/dist-server/service/actuation/twin-command.js +170 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -0
  99. package/dist-server/service/actuation/twin-now.d.ts +41 -0
  100. package/dist-server/service/actuation/twin-now.js +65 -0
  101. package/dist-server/service/actuation/twin-now.js.map +1 -0
  102. package/dist-server/service/index.d.ts +5 -2
  103. package/dist-server/service/index.js +28 -17
  104. package/dist-server/service/index.js.map +1 -1
  105. package/dist-server/service/reference/actuation-routing.d.ts +60 -0
  106. package/dist-server/service/reference/actuation-routing.js +126 -0
  107. package/dist-server/service/reference/actuation-routing.js.map +1 -0
  108. package/dist-server/service/reference/actuation-target.d.ts +65 -0
  109. package/dist-server/service/reference/actuation-target.js +94 -0
  110. package/dist-server/service/reference/actuation-target.js.map +1 -0
  111. package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
  112. package/dist-server/service/reference/connection-portability-resolver.js +181 -0
  113. package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
  114. package/dist-server/service/reference/connection-portability.d.ts +138 -0
  115. package/dist-server/service/reference/connection-portability.js +230 -0
  116. package/dist-server/service/reference/connection-portability.js.map +1 -0
  117. package/dist-server/service/reference/fill-cadence.d.ts +17 -0
  118. package/dist-server/service/reference/fill-cadence.js +31 -0
  119. package/dist-server/service/reference/fill-cadence.js.map +1 -0
  120. package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
  121. package/dist-server/service/reference/fill-dedupe.js +63 -0
  122. package/dist-server/service/reference/fill-dedupe.js.map +1 -0
  123. package/dist-server/service/reference/fill-loop.d.ts +35 -0
  124. package/dist-server/service/reference/fill-loop.js +187 -0
  125. package/dist-server/service/reference/fill-loop.js.map +1 -0
  126. package/dist-server/service/reference/fill-span.d.ts +10 -0
  127. package/dist-server/service/reference/fill-span.js +37 -0
  128. package/dist-server/service/reference/fill-span.js.map +1 -0
  129. package/dist-server/service/reference/hook-contract.d.ts +149 -22
  130. package/dist-server/service/reference/hook-contract.js +236 -23
  131. package/dist-server/service/reference/hook-contract.js.map +1 -1
  132. package/dist-server/service/reference/hook-rejected.d.ts +25 -0
  133. package/dist-server/service/reference/hook-rejected.js +48 -0
  134. package/dist-server/service/reference/hook-rejected.js.map +1 -0
  135. package/dist-server/service/reference/hook-store.d.ts +3 -0
  136. package/dist-server/service/reference/hook-store.js +28 -0
  137. package/dist-server/service/reference/hook-store.js.map +1 -0
  138. package/dist-server/service/reference/index.d.ts +4 -1
  139. package/dist-server/service/reference/index.js +5 -1
  140. package/dist-server/service/reference/index.js.map +1 -1
  141. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  142. package/dist-server/service/reference/live-cadence.js +44 -0
  143. package/dist-server/service/reference/live-cadence.js.map +1 -0
  144. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  145. package/dist-server/service/reference/live-feed-lease.js +56 -0
  146. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  147. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  148. package/dist-server/service/reference/live-ingest.js +66 -0
  149. package/dist-server/service/reference/live-ingest.js.map +1 -0
  150. package/dist-server/service/reference/reference-adapter.d.ts +355 -2
  151. package/dist-server/service/reference/reference-adapter.js +19 -1
  152. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  153. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  154. package/dist-server/service/reference/reference-fill-run.js +142 -0
  155. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  156. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  157. package/dist-server/service/reference/reference-fill.js +141 -0
  158. package/dist-server/service/reference/reference-fill.js.map +1 -0
  159. package/dist-server/service/reference/reference-hook.d.ts +64 -6
  160. package/dist-server/service/reference/reference-hook.js +265 -27
  161. package/dist-server/service/reference/reference-hook.js.map +1 -1
  162. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  163. package/dist-server/service/reference/reference-link-groups.js +15 -0
  164. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  165. package/dist-server/service/reference/reference-links.d.ts +14 -0
  166. package/dist-server/service/reference/reference-links.js +73 -0
  167. package/dist-server/service/reference/reference-links.js.map +1 -0
  168. package/dist-server/service/reference/reference-live.d.ts +1 -0
  169. package/dist-server/service/reference/reference-live.js +32 -6
  170. package/dist-server/service/reference/reference-live.js.map +1 -1
  171. package/dist-server/service/reference/reference-master.d.ts +56 -2
  172. package/dist-server/service/reference/reference-master.js +33 -3
  173. package/dist-server/service/reference/reference-master.js.map +1 -1
  174. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  175. package/dist-server/service/reference/reference-resolver.js +79 -1
  176. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  178. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  179. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  180. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  181. package/dist-server/service/twin-event/twin-event.js +11 -2
  182. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  183. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  184. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  185. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  186. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  187. package/dist-server/service/twin-model/external-resolver.js +109 -0
  188. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  189. package/dist-server/service/twin-model/project-structure.js +16 -3
  190. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  192. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  193. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  194. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  195. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  196. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  197. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  200. package/dist-server/service/twin-space/twin-space.js +13 -0
  201. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  202. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  203. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  204. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  205. package/dist-shared/entity-delta.js +1 -1
  206. package/dist-shared/entity-delta.js.map +1 -1
  207. package/package.json +7 -7
  208. package/server/engine/canonical-ingest.ts +119 -33
  209. package/server/engine/event-time.ts +96 -0
  210. package/server/engine/index.ts +20 -0
  211. package/server/engine/ingest-dedupe.ts +203 -27
  212. package/server/engine/ingest-health.ts +91 -6
  213. package/server/engine/kpi-fold.ts +24 -1
  214. package/server/engine/kpi-query.ts +23 -4
  215. package/server/engine/oee-accumulator.ts +80 -8
  216. package/server/engine/twin-engine.ts +119 -5
  217. package/server/index.ts +23 -0
  218. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  219. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  220. package/server/migrations/index.ts +5 -1
  221. package/server/routes.ts +19 -20
  222. package/server/service/actuation/actuation-advisor.ts +124 -0
  223. package/server/service/actuation/actuation-outcome.ts +97 -0
  224. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  225. package/server/service/actuation/actuation-rule.ts +139 -0
  226. package/server/service/actuation/approval-gateway.ts +44 -0
  227. package/server/service/actuation/catch-up-effects.ts +152 -0
  228. package/server/service/actuation/command-dispatcher.ts +511 -0
  229. package/server/service/actuation/command-store.ts +236 -0
  230. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  231. package/server/service/actuation/effect-match.ts +124 -0
  232. package/server/service/actuation/index.ts +27 -0
  233. package/server/service/actuation/retry-plan.ts +145 -0
  234. package/server/service/actuation/rule-evaluate.ts +352 -0
  235. package/server/service/actuation/rule-loop.ts +160 -0
  236. package/server/service/actuation/rule-runner.ts +216 -0
  237. package/server/service/actuation/rule-wiring.ts +84 -0
  238. package/server/service/actuation/settle-effects.ts +115 -0
  239. package/server/service/actuation/sweep-dispatch.ts +179 -0
  240. package/server/service/actuation/twin-command-resolver.ts +387 -0
  241. package/server/service/actuation/twin-command.ts +232 -0
  242. package/server/service/actuation/twin-now.ts +97 -0
  243. package/server/service/index.ts +11 -0
  244. package/server/service/reference/actuation-routing.ts +165 -0
  245. package/server/service/reference/actuation-target.ts +163 -0
  246. package/server/service/reference/connection-portability-resolver.ts +171 -0
  247. package/server/service/reference/connection-portability.ts +285 -0
  248. package/server/service/reference/fill-cadence.ts +29 -0
  249. package/server/service/reference/fill-dedupe.ts +66 -0
  250. package/server/service/reference/fill-loop.ts +153 -0
  251. package/server/service/reference/fill-span.ts +31 -0
  252. package/server/service/reference/hook-contract.ts +280 -22
  253. package/server/service/reference/hook-rejected.ts +53 -0
  254. package/server/service/reference/hook-store.ts +28 -0
  255. package/server/service/reference/index.ts +5 -1
  256. package/server/service/reference/live-cadence.ts +129 -0
  257. package/server/service/reference/live-feed-lease.ts +56 -0
  258. package/server/service/reference/live-ingest.ts +94 -0
  259. package/server/service/reference/reference-adapter.ts +392 -5
  260. package/server/service/reference/reference-fill-run.ts +165 -0
  261. package/server/service/reference/reference-fill.ts +223 -0
  262. package/server/service/reference/reference-hook.ts +355 -32
  263. package/server/service/reference/reference-link-groups.ts +21 -0
  264. package/server/service/reference/reference-links.ts +77 -0
  265. package/server/service/reference/reference-live.ts +30 -5
  266. package/server/service/reference/reference-master.ts +90 -5
  267. package/server/service/reference/reference-resolver.ts +77 -2
  268. package/server/service/twin-event/twin-event-keys.ts +37 -4
  269. package/server/service/twin-event/twin-event.ts +18 -1
  270. package/server/service/twin-model/choose-recipe.ts +31 -0
  271. package/server/service/twin-model/external-resolver.ts +161 -0
  272. package/server/service/twin-model/project-structure.ts +16 -3
  273. package/server/service/twin-model/twin-equipment.ts +22 -0
  274. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  275. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  276. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  277. package/server/service/twin-space/twin-space.ts +39 -0
  278. package/server/service/twin-subject/event-subjects.ts +8 -1
  279. package/shared/entity-delta.ts +1 -1
  280. package/test/ack-shape-alignment.test.ts +97 -0
  281. package/test/actuation-approval-door.test.ts +421 -0
  282. package/test/actuation-dispatch.test.ts +209 -0
  283. package/test/actuation-effect.test.ts +162 -0
  284. package/test/actuation-outcome.test.ts +152 -0
  285. package/test/actuation-rule-runner.test.ts +281 -0
  286. package/test/actuation-rule.test.ts +266 -0
  287. package/test/actuation-seam.test.ts +249 -0
  288. package/test/actuation-settle.test.ts +164 -0
  289. package/test/actuation-target.test.ts +145 -0
  290. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  291. package/test/capability-mapping.test.ts +1 -1
  292. package/test/choose-recipe.test.ts +117 -0
  293. package/test/command-store-writes-columns.test.ts +126 -0
  294. package/test/connection-portability-doors.test.ts +87 -0
  295. package/test/connector-capability-declaration.test.ts +54 -4
  296. package/test/declaration-reaches-model.test.ts +43 -0
  297. package/test/dedupe-transformation.test.ts +112 -0
  298. package/test/entity-delta.test.ts +1 -1
  299. package/test/event-subjects.test.ts +2 -1
  300. package/test/external-resolver.test.ts +171 -0
  301. package/test/fill-dedupe.test.ts +59 -0
  302. package/test/fill-loop.test.ts +35 -0
  303. package/test/first-envelope.test.ts +153 -0
  304. package/test/fold-shift-dst.test.ts +110 -0
  305. package/test/home-in-area.test.ts +90 -0
  306. package/test/hook-rejected-shape.test.ts +86 -0
  307. package/test/hook-response-sum.test.ts +193 -0
  308. package/test/hook-sequence.test.ts +333 -0
  309. package/test/ingest-dedupe.test.ts +56 -0
  310. package/test/ingest-expected-quiet.test.ts +111 -0
  311. package/test/ingest-idempotent.test.ts +79 -0
  312. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  313. package/test/ingest-rules-header.test.ts +100 -0
  314. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  315. package/test/intake-mapping-parity.test.ts +175 -0
  316. package/test/live-cadence.test.ts +170 -0
  317. package/test/live-feed-lease.test.ts +63 -0
  318. package/test/live-mirror-parity.test.ts +1 -1
  319. package/test/oee-accumulator.test.ts +3 -3
  320. package/test/oee-site-calendar.test.ts +146 -0
  321. package/test/order-and-transaction-split.test.ts +107 -0
  322. package/test/reference-fill.test.ts +235 -0
  323. package/test/reference-hook.test.ts +114 -23
  324. package/test/reference-links.test.ts +56 -0
  325. package/test/retry-plan.test.ts +139 -0
  326. package/test/sweep-dispatch.test.ts +191 -0
  327. package/test/twin-event-keys.test.ts +20 -6
  328. package/test/twin-model-item-db.test.ts +6 -6
  329. package/test/twin-model-tree-db.test.ts +6 -6
  330. package/tsconfig.shared.tsbuildinfo +1 -1
  331. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,87 @@
1
+ /*
2
+ * ★ **연결 이관의 문 셋** — 몸통은 순수 함수이고, 이 검사는 **문이 그 규율을 지키는지** 본다.
3
+ *
4
+ * ── 무엇을 옮기나 (사용자와 맞춘 개념, 2026-09-05) ────────────────────────
5
+ * 옮기는 것은 「붙는 법」이지 「붙은 결과」가 아니다. 마스터·사실·잰 값·비밀값은 안 옮긴다 —
6
+ * 앞의 셋은 다시 얻을 수 있고, 비밀값은 그 환경의 것이지 셋업이 아니다.
7
+ *
8
+ * ── 이 검사가 지키는 것 ────────────────────────────────────────────────────
9
+ * 문이 몸통의 판단을 **뒤집지 않는지**다. 몸통 시험 19개가 이미 「비밀값이 안 나간다」를 지키는데,
10
+ * 문이 그것을 되살려 쓰면 그 19개가 아무것도 못 막는다.
11
+ */
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+
15
+ import { exportConnections, planImport, SECRET_KEYS } from '../server/service/reference/connection-portability.js'
16
+
17
+ const row = (over: any = {}) => ({
18
+ source: 'mes-miratek',
19
+ system: 'operato-plant',
20
+ siteName: '미라텍',
21
+ adapterType: 'operato-plant',
22
+ connectionConfig: { baseUrl: 'https://plant.example', siteId: 'mes-miratek', token: 'SECRET-VALUE', hookSecret: 'ALSO-SECRET' },
23
+ ...over
24
+ })
25
+
26
+ test('★ 비밀값이 꾸러미에 안 나간다 — 이름만 나간다', () => {
27
+ const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
28
+ const text = JSON.stringify(b)
29
+
30
+ assert.ok(!text.includes('SECRET-VALUE'), '토큰이 꾸러미에 실렸다')
31
+ assert.ok(!text.includes('ALSO-SECRET'), '훅 비밀값이 꾸러미에 실렸다')
32
+ assert.deepEqual(b.connections[0].secretsRequired.sort(), ['hookSecret', 'token'], '무엇을 채워야 하는지 말하지 않는다')
33
+ })
34
+
35
+ test('★ 선언은 나간다 — 그것이 옮기려는 것이다', () => {
36
+ const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
37
+ const c = b.connections[0]
38
+
39
+ assert.equal(c.adapterType, 'operato-plant')
40
+ assert.equal(c.config.baseUrl, 'https://plant.example')
41
+ assert.equal(c.config.siteId, 'mes-miratek')
42
+ })
43
+
44
+ test('★ 새 비밀 칸도 기본으로 안 나간다 — 이름 규칙으로 거른다', () => {
45
+ /*
46
+ * 비밀 칸이 하나 늘 때 이 목록을 안 고치면 그 값이 파일로 나간다. 그래서 규칙이 이름이고,
47
+ * 그 규칙이 지금 무엇을 아는지 값으로 못 박는다.
48
+ */
49
+ assert.ok(SECRET_KEYS.includes('token' as any))
50
+ assert.ok(SECRET_KEYS.includes('hookSecret' as any))
51
+ assert.ok(SECRET_KEYS.includes('intakeSecret' as any))
52
+ assert.ok(SECRET_KEYS.length >= 4, `비밀 칸 이름을 ${SECRET_KEYS.length}개만 안다`)
53
+ })
54
+
55
+ test('★ 없는 비밀값을 지어내지 않는다 — 이름을 대고 사람에게 넘긴다', () => {
56
+ const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
57
+ /* 이 환경에는 그 비밀값이 없다. */
58
+ const plan = planImport(b, [], () => false)
59
+
60
+ const needs = plan.needsHuman.find(n => n.source === 'mes-miratek')
61
+ assert.ok(needs, '사람이 채울 것을 말하지 않는다')
62
+ assert.deepEqual(needs!.keys.sort(), ['hookSecret', 'token'])
63
+ })
64
+
65
+ test('★ 이미 있는 연결의 비밀값을 꾸러미가 안 지운다', () => {
66
+ const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
67
+ const existing = [{ source: 'mes-miratek', connectionConfig: { token: 'LIVE-TOKEN' } }]
68
+ const plan = planImport(b, existing, () => false)
69
+
70
+ const a = plan.actions.find(x => (x as any).connection?.source === 'mes-miratek') as any
71
+ assert.equal(a?.action, 'update', `갱신이 아니라 ${a?.action} 이다`)
72
+ assert.ok(a.keptSecrets?.includes('token'), '이미 있는 토큰을 지킨다고 말하지 않는다')
73
+ })
74
+
75
+ test('★ 꾸러미가 아닌 것을 받으면 아무것도 하지 않는다', () => {
76
+ for (const bad of [null, {}, { kind: 'something-else' }, 'text']) {
77
+ const plan = planImport(bad, [], () => false)
78
+ assert.equal(plan.actions.length, 0, `${JSON.stringify(bad)} 로 무언가를 하려 했다`)
79
+ assert.ok(plan.errors.length, '왜 안 되는지 말하지 않는다')
80
+ }
81
+ })
82
+
83
+ test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
84
+ const b = exportConnections([row()], { at: '2026-09-05T00:00:00.000Z' })
85
+ assert.equal(b.connections.length, 1, '꾸러미가 비었다 — 이 검사가 아무것도 안 잰다')
86
+ assert.ok(planImport(b, [], () => true).actions.length > 0, '계획이 비었다')
87
+ })
@@ -50,19 +50,69 @@ test('★ 라이브를 구현하고 선언하지 않으면 말해 준다 — 이
50
50
  assert.ok(/live twin cannot be created/.test(said))
51
51
  })
52
52
 
53
- test('★ 조치를 구현하고 선언하지 않아도 말해 준다 — 같은 규율이다', () => {
53
+ test('★ 구동을 구현하고 선언하지 않아도 말해 준다 — 같은 규율이다', () => {
54
54
  const before = warnings.length
55
55
  const a = adapter({ control: async () => ({ ok: true }) })
56
56
  registerAdapterType(a)
57
57
  assert.ok(warnings.slice(before).join('\n').includes('control'))
58
58
  })
59
59
 
60
- test(' 어긋나면 말한다하나만 말하면 고친 막힌다', () => {
60
+ test(' 조치를 구현하고 선언하지 않아도 말해 준다 이름이 없어서 오늘 하루가 갔다', () => {
61
+ /*
62
+ * 이 자리에 `actuate` 가 없었다. 능력 축이 `live`·`control` 둘뿐이라 커넥터는 조치를 **선언할
63
+ * 방법이 없었고**, 등록 검사도 그것을 물을 수 없었다. MES 커넥터는 그 사이에 `actuate` 를 다
64
+ * 만들어 두었는데, 넘기는 자리는 「구현이 없습니다」로 답했다.
65
+ *
66
+ * 그 문장이 그럴듯해서 설계 문서와 다른 레인에까지 사실로 옮겨졌다. 오류가 아니라 **정직해 보이는
67
+ * 거절**이 원인이었다.
68
+ */
61
69
  const before = warnings.length
62
- const a = adapter({ openLiveFeed: () => () => {}, control: async () => ({ ok: true }) })
70
+ const a = adapter({ actuate: async () => ({ ok: true }) })
63
71
  registerAdapterType(a)
72
+
64
73
  const said = warnings.slice(before).join('\n')
65
- assert.ok(said.includes('live') && said.includes('control'))
74
+ assert.ok(said.includes('actuate'), '무엇이 빠졌는지 말한다')
75
+ assert.ok(said.includes(a.type), '어느 커넥터인지 말한다')
76
+ })
77
+
78
+ test('여럿이 어긋나면 다 말한다 — 하나만 말하면 고친 뒤 또 막힌다', () => {
79
+ const before = warnings.length
80
+ const a = adapter({
81
+ openLiveFeed: () => () => {},
82
+ control: async () => ({ ok: true }),
83
+ actuate: async () => ({ ok: true })
84
+ })
85
+ registerAdapterType(a)
86
+
87
+ const said = warnings.slice(before).join('\n')
88
+ for (const name of ['live', 'control', 'actuate']) {
89
+ assert.ok(said.includes(name), `${name} 를 말하지 않았다`)
90
+ }
91
+ })
92
+
93
+ test('★ 능력 이름이 늘면 검사도 함께 는다 — 표 하나에서 돈다', () => {
94
+ /*
95
+ * 예전에는 검사할 능력 셋이 코드 안에 손으로 적혀 있었다(`['live','control']`). 새 능력을 넣을 때
96
+ * 판정에는 넣고 검사에는 잊으면, **구현했는데 선언 안 한 것을 아무도 말해 주지 않는다** — 오늘 그
97
+ * 상태였다.
98
+ *
99
+ * 지금은 능력↔메서드 표 하나에서 돈다. 이 시험은 그 표가 실제로 셋을 다 덮는지 본다 — 넷째 능력을
100
+ * 넣고 표에 안 적으면 여기서 걸린다.
101
+ */
102
+ const cases: [string, Record<string, unknown>][] = [
103
+ ['live', { openLiveFeed: () => () => {} }],
104
+ ['control', { control: async () => ({ ok: true }) }],
105
+ ['actuate', { actuate: async () => ({ ok: true }) }]
106
+ ]
107
+
108
+ for (const [name, impl] of cases) {
109
+ const before = warnings.length
110
+ registerAdapterType(adapter(impl))
111
+ assert.ok(
112
+ warnings.slice(before).join('\n').includes(name),
113
+ `${name} 를 구현하고 선언하지 않았는데 아무 말도 하지 않았다 — 검사 표에 그 이름이 없다`
114
+ )
115
+ }
66
116
  })
67
117
 
68
118
  test('선언과 구현이 맞으면 아무 말도 하지 않는다 — 소음이 되면 아무도 읽지 않는다', () => {
@@ -88,6 +88,9 @@ const VALUE: Record<string, unknown> = {
88
88
  gs1Id: 'urn:epc:id:sgln:0614141.00001.0',
89
89
  /* 설비의 정체성 — **GS1 키가 아닌 것이 정상이다**(실측: 인버터 하드웨어 식별자). */
90
90
  identity: 'ES100000007d8d061d-1',
91
+ /* 설비 등급 소속 — 2026-09-04 에 이 칸이 접수면에 없어서 원본이 채운 등급 22대가 문턱에서
92
+ 버려졌다(인티그레이션 레인 실측). 사람·자산은 받는데 설비만 없었다. */
93
+ equipmentClassIds: ['welding-robot'],
91
94
  homeLocationId: 'cut-station',
92
95
  mtbfMs: 600000,
93
96
  mttrMs: 60000,
@@ -178,3 +181,43 @@ for (const c of CASES) {
178
181
  )
179
182
  })
180
183
  }
184
+
185
+ /*
186
+ * ── 공간의 시각 기준도 모델까지 간다 (2026-09-07) ────────────────────────────
187
+ *
188
+ * 위 검사는 **자원**의 필드만 본다. `space` 는 안 보고 있었고, 그래서 실제로 하나가 새고 있었다 —
189
+ * `space.timezone` 이 오프셋으로만 풀려 모델에 실리고 **이름은 버려졌다.**
190
+ *
191
+ * 이름이 없으면 소비처가 순간마다 다시 풀 수 없다. 지난 구간을 교대별로 접을 때 그 구간 안의
192
+ * 일광절약시간 전환이 사실을 한 시간 밀어 **다른 교대로** 세게 하고, 오류는 나지 않는다.
193
+ * `America/Tijuana`(PST −480 / PDT −480 아닌 −420)가 이미 자료에 있다.
194
+ *
195
+ * 그리고 이 새는 자리를 잡은 것은 타입 검사도, 위 하네스도 아니라 **다른 레인의 물음**이었다
196
+ * ("자료를 채운 뒤 이미 세워진 모델이 따라가나"). 그래서 여기 검사로 남긴다.
197
+ */
198
+ const withSpace = (space: Record<string, unknown>) => masterToTwin({ ...master(), space: { ...master().space, ...space } })
199
+
200
+ test('공간의 시간대 이름이 모델까지 간다 — 오프셋만 실으면 계절이 바뀔 때 틀린다', () => {
201
+ const { model } = withSpace({ timezone: 'America/Tijuana' })
202
+
203
+ assert.equal((model as any).timezone, 'America/Tijuana', '이름이 모델에 없다 — 소비처가 순간마다 풀 수 없다')
204
+ assert.equal(typeof (model as any).utcOffsetMinutes, 'number', '오프셋도 함께 실려야 한다 — 이름을 안 보는 소비처가 그 값으로 돈다')
205
+ })
206
+
207
+ test('시간대를 선언하지 않으면 두 칸 다 만들지 않는다 — 0 은 「UTC 라고 선언했다」는 주장이다', () => {
208
+ const { model } = withSpace({})
209
+
210
+ assert.equal((model as any).timezone, undefined)
211
+ assert.equal((model as any).utcOffsetMinutes, undefined)
212
+ })
213
+
214
+ test('풀리지 않는 이름은 싣지 않고 경고로 말한다 — 실어 두면 소비처가 믿고 매번 실패한다', () => {
215
+ const { model, warnings } = withSpace({ timezone: 'Mars/Olympus_Mons' })
216
+
217
+ assert.equal((model as any).timezone, undefined)
218
+ assert.equal((model as any).utcOffsetMinutes, undefined)
219
+ assert.ok(
220
+ (warnings ?? []).some((w: any) => JSON.stringify(w).includes('Mars/Olympus_Mons')),
221
+ '모르는 시간대를 조용히 버리지 않는다'
222
+ )
223
+ })
@@ -0,0 +1,112 @@
1
+ /*
2
+ * 변환·담김의 정체가 **무엇이 무엇으로 되었나**로 갈리나.
3
+ *
4
+ * ── 왜 필요한가 (2026-09-07) ────────────────────────────────────────────────
5
+ * 이 자리가 없어서 **사실 하나가 없어졌습니다.** 거절이 아니라 되풀이로 세어져서 경고도 없었습니다.
6
+ *
7
+ * ```
8
+ * plant outbox seq 37 in LOT-CT-SIDE-001 24개 → out LOT-CT-MAIN-003 12개
9
+ * plant outbox seq 42 in LOT-CT-SIDE-001 18개 → out LOT-CT-MAIN-004 9개
10
+ * transformationID · bizStep · 초까지 전부 같다
11
+ * 저널에 남은 것 LOT-CT-MAIN-003 하나
12
+ * ```
13
+ *
14
+ * `LOT-CT-MAIN-004` 를 어느 패널로 만들었는지 트윈이 답하지 못했습니다. 인티그레이션 레인이
15
+ * 원본과 저널의 건수를 맞춰 보다 찾았습니다 — 4건이 나갔고 3건이 앉았습니다.
16
+ *
17
+ * 아래 값은 그 두 봉투에서 온 것입니다. 지어낸 모양이 아닙니다.
18
+ */
19
+ import test from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+
22
+ import { factIdentity } from '../server/engine/ingest-dedupe.js'
23
+
24
+ const AT = '2026-09-05T05:00:00.000Z'
25
+
26
+ const transformation = (inQty: number, outClass: string, outQty: number) => ({
27
+ eventId: `evt-${outClass}-${outQty}`,
28
+ eventType: 'epcis.TransformationEvent',
29
+ eventTime: AT,
30
+ data: {
31
+ type: 'TransformationEvent',
32
+ bizStep: 'urn:epcglobal:cbv:bizstep:commissioning',
33
+ transformationID: 'WO-20260831-001',
34
+ inputQuantityList: [{ epcClass: 'LOT-CT-SIDE-001', quantity: inQty }],
35
+ outputQuantityList: [{ epcClass: outClass, quantity: outQty }]
36
+ }
37
+ })
38
+
39
+ test('산출 로트가 다르면 다른 사실이다 — 이것이 실제로 없어진 자리다', () => {
40
+ const a = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
41
+ const b = factIdentity(transformation(18, 'LOT-CT-MAIN-004', 9))
42
+
43
+ assert.ok(a, '정체를 만들지 못하면 dedupe 밖이다')
44
+ assert.notEqual(a, b, '같은 오더·같은 공정·같은 초라도 산출이 다르면 다른 변환이다')
45
+ })
46
+
47
+ test('똑같은 봉투가 두 번 오면 한 사실이다 — 갈라 놓기만 하면 되풀이를 못 막는다', () => {
48
+ assert.equal(
49
+ factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12)),
50
+ factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
51
+ )
52
+ })
53
+
54
+ test('산출 이름이 같고 수량만 다르면 다른 사실이다 — 분할이 그 모양이다', () => {
55
+ const a = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
56
+ const b = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 6))
57
+
58
+ assert.notEqual(a, b)
59
+ })
60
+
61
+ test('입력 수량만 다르면 다른 사실이다 — 산출이 같아도 소비가 다르다', () => {
62
+ const a = factIdentity(transformation(24, 'LOT-CT-MAIN-003', 12))
63
+ const b = factIdentity(transformation(18, 'LOT-CT-MAIN-003', 12))
64
+
65
+ assert.notEqual(a, b)
66
+ })
67
+
68
+ test('오더가 다르면 다른 사실이다 — 같은 공정을 두 오더로 같은 초에 돌릴 수 있다', () => {
69
+ const withTx = (tx: string) => {
70
+ const e = transformation(24, 'LOT-CT-MAIN-003', 12) as any
71
+ e.data.bizTransactionList = [{ type: 'urn:epcglobal:cbv:btt:prodorder', bizTransaction: tx }]
72
+ return factIdentity(e)
73
+ }
74
+
75
+ assert.notEqual(withTx('urn:x:bt:WO-A'), withTx('urn:x:bt:WO-B'))
76
+ })
77
+
78
+ test('담김의 자식이 다르면 다른 사실이다 — 한 종류만 고치면 나머지에 같은 결함이 남는다', () => {
79
+ const aggregation = (child: string) => ({
80
+ eventId: `agg-${child}`,
81
+ eventType: 'epcis.AggregationEvent',
82
+ eventTime: AT,
83
+ data: {
84
+ type: 'AggregationEvent',
85
+ action: 'ADD',
86
+ bizStep: 'urn:epcglobal:cbv:bizstep:packing',
87
+ parentID: 'urn:epc:id:sscc:0614141.1234567890',
88
+ childQuantityList: [{ epcClass: child, quantity: 10 }]
89
+ }
90
+ })
91
+
92
+ assert.notEqual(factIdentity(aggregation('LOT-A')), factIdentity(aggregation('LOT-B')))
93
+ })
94
+
95
+ test('수량 목록 전체를 본다 — 첫 원소만 보면 뒤가 다른 둘이 뭉친다', () => {
96
+ const twoLines = (second: string) => ({
97
+ eventId: `evt-${second}`,
98
+ eventType: 'epcis.TransformationEvent',
99
+ eventTime: AT,
100
+ data: {
101
+ type: 'TransformationEvent',
102
+ bizStep: 'urn:epcglobal:cbv:bizstep:commissioning',
103
+ transformationID: 'WO-20260831-001',
104
+ outputQuantityList: [
105
+ { epcClass: 'LOT-SAME', quantity: 1 },
106
+ { epcClass: second, quantity: 1 }
107
+ ]
108
+ }
109
+ })
110
+
111
+ assert.notEqual(factIdentity(twoLines('LOT-X')), factIdentity(twoLines('LOT-Y')))
112
+ })
@@ -47,7 +47,7 @@ test('무버: OEE 는 정수 퍼센트로 정형화하고 원시 누적은 빼
47
47
  id: 'cutter-1',
48
48
  kind: 'cutter',
49
49
  status: 'down',
50
- oee: { overall: 0.8234, availability: 0.9, performance: 0.95, quality: 0.96, goodCount: 12, scrapCount: 3, idleMs: 99999 }
50
+ oee: { overall: 0.8234, availability: 0.9, performance: 0.95, quality: 0.96, goodCount: 12, nonconformingCount: 3, idleMs: 99999 }
51
51
  }
52
52
  ]
53
53
  }, 't1')
@@ -91,7 +91,8 @@ test('★ 같은 대상을 두 번 적지 않는다 — 출발지와 도착지
91
91
  })
92
92
 
93
93
  test('종류 목록이 닫혀 있다 — 열어 두면 넣는 쪽마다 다른 이름을 쓴다', () => {
94
- assert.deepEqual([...SUBJECT_KINDS], ['item', 'lot', 'order', 'location', 'equipment', 'task'])
94
+ /* `transaction` 2026-09-05 늘었다 — 거래가 오더 칸에 섞여 들어오던 것을 갈랐다. */
95
+ assert.deepEqual([...SUBJECT_KINDS], ['item', 'lot', 'order', 'location', 'equipment', 'task', 'transaction'])
95
96
  })
96
97
 
97
98
  test('너무 긴 식별자는 적지 않는다 — 자르면 그 줄이 다른 대상을 가리킨다', () => {
@@ -0,0 +1,171 @@
1
+ /*
2
+ * ★ **밖을 가리키는 관계를 푸는 자리** — 선언은 있고 코드가 0곳이던 이음새.
3
+ *
4
+ * ── 무엇이 있었나 (2026-09-05 실측) ────────────────────────────────────────
5
+ * 계약이 `target: { kind: 'external', entity: … }` 를 선언하고 주석에 「해소는 호스트가 한다」고
6
+ * 적어 두었다. 그런데 `headless-twin/server` 어디에도 그것을 다루는 코드가 없었다(0곳).
7
+ *
8
+ * `space.area` 가 도는 것처럼 보인 이유는 이음새가 있어서가 아니라 **호스트에서 그것이 평범한 FK
9
+ * 라서**다 — 선언이 길을 낸 것이 아니다.
10
+ *
11
+ * ── 왜 지금 채웠나 ─────────────────────────────────────────────────────────
12
+ * 아침에는 「아무도 안 막혔으니 급하지 않다」로 적었고 **그 판단이 규모를 작게 잡은 것**이었다.
13
+ *
14
+ * 360 의 소비처는 무한히 늘어난다(사용자 방향). 그러면 각 제품의 자기 저장소가 관계의 원천이 된다.
15
+ * 그때 선언은 「밖을 가리킨다」고 말하는데 푸는 길이 없으면 **제품마다 자기 길을 낸다** — 그것이
16
+ * 정확히 「무한히 갈린다」다.
17
+ */
18
+ import { test } from 'node:test'
19
+ import assert from 'node:assert/strict'
20
+
21
+ import {
22
+ clearExternalResolvers,
23
+ externalIncoming,
24
+ externalResolverEntities,
25
+ registerExternalResolver,
26
+ resolveExternal
27
+ } from '../server/service/twin-model/external-resolver.js'
28
+
29
+ const AREA = 'space.area'
30
+
31
+ /* ── 못 풀면 못 푼다고 말한다 ────────────────────────────────────────────── */
32
+
33
+ test('★ 꽂힌 것이 없으면 「해소기가 없다」고 말한다 — 「없다」가 아니다', async () => {
34
+ /*
35
+ * 링크를 지우면 화면은 「그런 관계가 없다」로 읽는다. 실제로는 「아무도 답할 수 없다」다.
36
+ * 사람이 할 일이 다르다 — 앞은 자료를 채울 일이고 뒤는 해소기를 꽂을 일이다.
37
+ */
38
+ clearExternalResolvers()
39
+ const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'])
40
+
41
+ assert.equal(got.unresolved, 'no-resolver')
42
+ assert.equal(got.refs, undefined)
43
+ })
44
+
45
+ test('★ 해소기가 터져도 360 을 죽이지 않는다 — 알리고 못 풀었다고 한다', async () => {
46
+ clearExternalResolvers()
47
+ const said: string[] = []
48
+ registerExternalResolver({ entity: AREA, resolve: async () => { throw new Error('DB down') } })
49
+
50
+ const got = await resolveExternal(AREA, 'd1', ['X'], m => said.push(m))
51
+
52
+ assert.equal(got.unresolved, 'failed')
53
+ assert.ok(said.some(m => m.includes('DB down')), '사유를 삼켰다')
54
+ })
55
+
56
+ /* ── 풀면 무엇인지 말한다 ────────────────────────────────────────────────── */
57
+
58
+ test('★ 이름과 갈 곳을 낸다 — 막다른 링크를 만들지 않으려고', async () => {
59
+ clearExternalResolvers()
60
+ registerExternalResolver({
61
+ entity: AREA,
62
+ resolve: async (_d, ids) => ids.map(id => ({ id, name: `라인 ${id}`, href: `/areas/${id}` }))
63
+ })
64
+
65
+ const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'])
66
+ assert.equal(got.refs?.[0].name, '라인 LINE-CAM')
67
+ assert.equal(got.refs?.[0].href, '/areas/LINE-CAM')
68
+ })
69
+
70
+ test('★ 없는 id 는 없다고 말한다 — 답에서 빼지 않는다', async () => {
71
+ /* 빼면 화면은 그 참조가 원래 없었던 것으로 읽는다. 실제로는 가리키는데 대상이 없는 것이다. */
72
+ clearExternalResolvers()
73
+ registerExternalResolver({ entity: AREA, resolve: async (_d, ids) => ids.map(id => ({ id, missing: true })) })
74
+
75
+ const got = await resolveExternal(AREA, 'd1', ['사라진구역'])
76
+ assert.equal(got.refs?.length, 1)
77
+ assert.equal(got.refs?.[0].missing, true)
78
+ })
79
+
80
+ /* ── 들어오는 쪽 ─────────────────────────────────────────────────────────── */
81
+
82
+ test('★ 제품이 자기 저장소의 관계를 싣는다 — 커널은 그 표를 모른다', async () => {
83
+ clearExternalResolvers()
84
+ registerExternalResolver({
85
+ entity: 'mes.records',
86
+ incoming: async () => [
87
+ { via: 'mes.rel.report', from: 'productionReports', tense: 'past', ids: ['PR-1', 'PR-2'], total: 2 },
88
+ { via: 'mes.rel.maintenance', from: 'maintenancePlans', tense: 'future', ids: ['MP-1'], total: 1 }
89
+ ]
90
+ })
91
+
92
+ const got = await externalIncoming('d1', 'mes-miratek', 'equipment', 'EQ-CT-MWELD-01')
93
+
94
+ assert.equal(got.length, 2)
95
+ assert.deepEqual(got.map(g => g.tense).sort(), ['future', 'past'], '시간축이 안 실렸다')
96
+ })
97
+
98
+ test('★ 시간축이 실린다 — 「걸린 것이 열」이 두 뜻이 되는 자리', async () => {
99
+ /*
100
+ * 지난 일 아홉 · 앞으로 올 일 하나와 그 반대는 「이 설비를 세워도 되나」의 답이 정반대다.
101
+ * 묶음이 스스로 말해야 화면이 가를 수 있다.
102
+ */
103
+ clearExternalResolvers()
104
+ registerExternalResolver({
105
+ entity: 'mes.records',
106
+ incoming: async () => [{ via: 'mes.rel.report', from: 'x', tense: 'past', ids: ['a'], total: 9 }]
107
+ })
108
+
109
+ const got = await externalIncoming('d1', 'i', 'equipment', 'E-1')
110
+ assert.equal(got[0].tense, 'past')
111
+ assert.equal(got[0].total, 9, '전체 수를 안 말한다 — 잘린 목록만으로는 규모를 모른다')
112
+ })
113
+
114
+ test('빈 묶음은 싣지 않는다 — 그 제품이 이 축을 안 보는 것이고 할 말이 없다', async () => {
115
+ clearExternalResolvers()
116
+ registerExternalResolver({
117
+ entity: 'mes.records',
118
+ incoming: async () => [{ via: 'x', from: 'y', ids: [], total: 0 }]
119
+ })
120
+ assert.deepEqual(await externalIncoming('d1', 'i', 'equipment', 'E-1'), [])
121
+ })
122
+
123
+ test('★ 한 제품이 터져도 나머지는 실린다', async () => {
124
+ clearExternalResolvers()
125
+ const said: string[] = []
126
+ registerExternalResolver({ entity: 'broken', incoming: async () => { throw new Error('boom') } })
127
+ registerExternalResolver({
128
+ entity: 'ok',
129
+ incoming: async () => [{ via: 'v', from: 'f', ids: ['a'], total: 1 }]
130
+ })
131
+
132
+ const got = await externalIncoming('d1', 'i', 'equipment', 'E-1', m => said.push(m))
133
+ assert.equal(got.length, 1, '하나가 터져서 나머지도 안 실렸다')
134
+ assert.ok(said.some(m => m.includes('boom')))
135
+ })
136
+
137
+ /* ── 꽂는 규율 ───────────────────────────────────────────────────────────── */
138
+
139
+ test('★ 같은 대상을 두 제품이 답하려 하면 알린다 — 조용히 덮지 않는다', async () => {
140
+ clearExternalResolvers()
141
+ const said: string[] = []
142
+ registerExternalResolver({ entity: AREA, resolve: async () => [] })
143
+ registerExternalResolver({ entity: AREA, resolve: async () => [] }, m => said.push(m))
144
+
145
+ assert.ok(said.some(m => m.includes(AREA)), '덮어쓴 것을 안 알렸다')
146
+ })
147
+
148
+ test('entity 없는 해소기는 안 꽂는다 — 아무 관계에도 안 붙는다', async () => {
149
+ clearExternalResolvers()
150
+ const said: string[] = []
151
+ registerExternalResolver({ entity: ' ' } as any, m => said.push(m))
152
+
153
+ assert.deepEqual(externalResolverEntities(), [])
154
+ assert.ok(said.length, '조용히 넘겼다')
155
+ })
156
+
157
+ test('★ 무엇을 풀 수 있나를 밖에서 잴 수 있다 — 「모른다」와 「없다」를 가르려고', async () => {
158
+ clearExternalResolvers()
159
+ registerExternalResolver({ entity: 'b', resolve: async () => [] })
160
+ registerExternalResolver({ entity: 'a', resolve: async () => [] })
161
+
162
+ assert.deepEqual(externalResolverEntities(), ['a', 'b'])
163
+ })
164
+
165
+ test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', async () => {
166
+ clearExternalResolvers()
167
+ assert.deepEqual(externalResolverEntities(), [], '지우기가 안 듣는다')
168
+ registerExternalResolver({ entity: AREA, resolve: async (_d, ids) => ids.map(id => ({ id })) })
169
+ const got = await resolveExternal(AREA, 'd', ['X'])
170
+ assert.equal(got.refs?.length, 1, '꽂았는데 안 불린다')
171
+ })
@@ -0,0 +1,59 @@
1
+ /*
2
+ * backfill 이 이미 있는 사실을 다시 앉히지 않게 하는 부분.
3
+ *
4
+ * ── 무엇이 있었나 (2026-09-06 측정) ─────────────────────────────────────────
5
+ * 중복 판정 기억은 프로세스 안에만 있다. 재기동하면 비워지고, 그 뒤 backfill 을 돌리면 이미 저널에
6
+ * 있는 사실도 「처음 본 것」이 된다.
7
+ *
8
+ * ```
9
+ * mes-line-a 를 재기동 뒤 backfill
10
+ * 저널 44 → 83건 · 서로 다른 것 39가지 · 중복 44가지
11
+ * 같은 프로세스에서 다시 돌리면 applied 0 (기억이 살아 있어서)
12
+ * ```
13
+ *
14
+ * 저널 조회는 DB 를 물어야 해서 여기서 다루지 않는다. 확인하는 것은 **어느 구간을 읽을지 정하는
15
+ * 판정**이다 — 그것이 틀리면 조회가 엉뚱한 곳을 보고 기억이 안 찬다.
16
+ */
17
+ import { test } from 'node:test'
18
+ import assert from 'node:assert/strict'
19
+
20
+ import { spanOf } from '../server/service/reference/fill-span.js'
21
+
22
+ const at = (s: string) => ({ subjectId: 'L', decision: 'scrap', at: s })
23
+
24
+ test('★ 레코드가 덮는 구간을 정한다', () => {
25
+ const span = spanOf([at('2026-09-01T00:00:00.000Z'), at('2026-09-03T12:00:00.000Z'), at('2026-09-02T00:00:00.000Z')])
26
+ assert.ok(span)
27
+ assert.equal(span!.from.toISOString(), '2026-08-31T23:59:59.000Z')
28
+ assert.equal(span!.to.toISOString(), '2026-09-03T12:00:01.000Z')
29
+ })
30
+
31
+ test('★ 양쪽을 1초씩 넓힌다 — 좁게 읽으면 경계의 사실이 다시 앉는다', () => {
32
+ /*
33
+ * 저장된 시각은 밀리초까지 있고 경계에서 같은 값이 갈릴 수 있다. 넓게 읽는 것은 기억이 조금 더
34
+ * 차는 것뿐이라 손해가 없다.
35
+ */
36
+ const one = '2026-09-01T00:00:00.000Z'
37
+ const span = spanOf([at(one)])!
38
+ assert.equal(span.to.getTime() - span.from.getTime(), 2000)
39
+ })
40
+
41
+ test('★ 하나도 시각을 안 말하면 구간이 없다 — 저널 전체를 읽지 않는다', () => {
42
+ /*
43
+ * 구간을 못 정하면 채우지 않는다. 저널 전체를 읽는 쪽으로 넓히면 2백만 줄을 훑게 되고, 그것이
44
+ * 필요한 상황이면 조회가 아니라 정체 색인으로 풀어야 한다.
45
+ */
46
+ assert.equal(spanOf([{ subjectId: 'L', decision: 'scrap' }]), undefined)
47
+ assert.equal(spanOf([]), undefined)
48
+ })
49
+
50
+ test('시각을 말하는 것과 안 말하는 것이 섞이면 말하는 것으로 정한다', () => {
51
+ const span = spanOf([{ subjectId: 'A' }, at('2026-09-01T00:00:00.000Z'), { subjectId: 'B' }])
52
+ assert.ok(span, '한 줄이라도 말하면 구간이 있다')
53
+ })
54
+
55
+ test('★ 이 테스트가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
56
+ const narrow = spanOf([at('2026-09-01T00:00:00.000Z')])!
57
+ const wide = spanOf([at('2026-09-01T00:00:00.000Z'), at('2026-09-10T00:00:00.000Z')])!
58
+ assert.ok(wide.to.getTime() > narrow.to.getTime(), '구간이 레코드를 안 따라간다')
59
+ })
@@ -0,0 +1,35 @@
1
+ /*
2
+ * 빠진 구간을 **언제** 다시 받나 — 세 자리의 규율.
3
+ *
4
+ * ```
5
+ * 1 재기동 직후 한 번 우리가 내려가 있는 동안 그쪽이 보낸 것
6
+ * 2 구멍을 본 그 자리 번호가 비었다는 것을 아는 가장 이른 순간
7
+ * 3 10분 그물 위 둘이 다 놓치는 경우
8
+ * ```
9
+ *
10
+ * 여기서 확인하는 것은 **두 번 부르지 않는 규율**이다. 밀어 받는 쪽이 구멍 뒤의 봉투를 계속 보내면
11
+ * 그때마다 구멍 판정이 나고, 막지 않으면 봉투마다 backfill 이 돈다.
12
+ *
13
+ * 실제 조회는 DB 와 어댑터를 물어야 해서 여기서 다루지 않는다.
14
+ */
15
+ import { test } from 'node:test'
16
+ import assert from 'node:assert/strict'
17
+
18
+ import { FILL_DEBOUNCE_MS, FILL_PERIOD_MS } from '../server/service/reference/fill-cadence.js'
19
+
20
+ test('★ 그물은 10분이다 — 30초가 아니다', () => {
21
+ /*
22
+ * 인티그레이션 레인이 측정으로 준 권고다. backfill 은 원본에 조회를 걸고 저널을 읽는다.
23
+ * 30초마다 돌면 얻는 것 없이 두 쪽을 계속 친다 — 놓친 구간은 10분 뒤에 받아도 사실이 달라지지 않는다.
24
+ *
25
+ * 규칙 주기(30초)에 얹지 않은 이유이기도 하다. 얹으면 간격이 그쪽에 묶이고, 나중에 규칙 주기를
26
+ * 바꾸면 이쪽이 소리 없이 따라간다.
27
+ */
28
+ assert.equal(FILL_PERIOD_MS, 10 * 60_000)
29
+ assert.notEqual(FILL_PERIOD_MS, 30_000, '규칙 주기와 같아졌다 — 그러면 하나를 바꿀 때 둘이 움직인다')
30
+ })
31
+
32
+ test('★ 같은 단위를 1분 안에 두 번 안 받는다 — 봉투마다 돌면 안 된다', () => {
33
+ assert.equal(FILL_DEBOUNCE_MS, 60_000)
34
+ assert.ok(FILL_DEBOUNCE_MS < FILL_PERIOD_MS, '막는 간격이 그물보다 길면 그물이 헛돈다')
35
+ })