@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,97 @@
1
+ /*
2
+ * ★ **셈이 닫혀야 한다** — 두 갈래로는 큰 수가 「사라졌다」로 떠 있게 된다.
3
+ *
4
+ * ── 실측 (2026-09-05, 인티그레이션·plant 레인) ─────────────────────────────
5
+ * plant 아웃박스와 저널을 손으로 맞춰 보니 정확히 닫혔다.
6
+ *
7
+ * 나간 것 157 = 앉은 것 46 + 거절 2 + 어휘 1 + 되풀이 108
8
+ *
9
+ * 되풀이 108은 하네스가 같은 실적을 같은 시각으로 다시 보낸 것이고 **합친 것이 옳은 거동**이다.
10
+ * 어휘 1은 저널에 안 가는 것이 옳다(시각축이 없다).
11
+ *
12
+ * ── 왜 두 갈래로는 안 되나 ────────────────────────────────────────────────
13
+ * 두 갈래 157 − 46 − 2 = 109 「설명 못 함」
14
+ * 네 갈래 157 − 46 − 2 − 108 − 1 = 0
15
+ *
16
+ * **109 를 「사라졌다」로 내면 그날부터 아무도 그 수를 안 본다.** 늘 큰 수가 떠 있으면 진짜로 하나
17
+ * 사라진 날을 못 본다 — 오늘 이 저장소에서 「늘 참인 경고는 벽지가 된다」로 여러 번 나온 것이다.
18
+ *
19
+ * ── 그리고 되풀이 108은 잰 값이 아니었다 ──────────────────────────────────
20
+ * 뺄셈으로만 구할 수 있었다. 어휘 항이 없어서다. 그 뺄셈을 코드가 하려면 넷이 다 있어야 한다.
21
+ */
22
+ import { test } from 'node:test'
23
+ import assert from 'node:assert/strict'
24
+
25
+ import {
26
+ newIngestLedger,
27
+ recordIngest,
28
+ recordDuplicates,
29
+ recordVocabulary,
30
+ recordJournalWrite
31
+ } from '../server/engine/ingest-health.js'
32
+
33
+ const T0 = 1_757_000_000_000
34
+
35
+ test('★ 넷을 다 세면 셈이 0으로 닫힌다 — 실측한 그 수로', () => {
36
+ const ledger = newIngestLedger()
37
+
38
+ /* 나간 것 157 · 거절 2 */
39
+ recordIngest(ledger, 157, [{ errors: ['x'] }, { errors: ['y'] }], T0)
40
+ /* 저널에 앉은 것 46 */
41
+ recordJournalWrite(ledger, 46, T0)
42
+ /* 합친 것 108 */
43
+ recordDuplicates(ledger, 108, T0)
44
+ /* 저널에 안 가는 어휘 1 */
45
+ recordVocabulary(ledger, 1)
46
+
47
+ const w = ledger.window
48
+ const unexplained = w.offered - w.rows - w.rejected - ledger.duplicates - w.vocabulary
49
+
50
+ assert.equal(unexplained, 0, `설명 못 한 것이 ${unexplained} 남았다 — 셈이 안 닫힌다`)
51
+ })
52
+
53
+ test('★ 어휘 항이 없으면 그 수가 「사라졌다」로 남는다 — 이 항의 존재 이유', () => {
54
+ const ledger = newIngestLedger()
55
+ recordIngest(ledger, 20, undefined, T0)
56
+ recordJournalWrite(ledger, 17, T0)
57
+ recordVocabulary(ledger, 1)
58
+
59
+ const w = ledger.window
60
+ /* 어휘를 안 빼면 셋이 빈 것으로 보인다 — 실제로 사라진 것은 둘이다. */
61
+ assert.equal(w.offered - w.rows, 3, '실측 상황을 못 만들고 있다')
62
+ assert.equal(w.offered - w.rows - w.vocabulary, 2, '어휘를 빼도 수가 안 맞는다')
63
+ })
64
+
65
+ test('★ 어휘는 저널 수에 안 들어간다 — 두 번 세면 셈이 음수가 된다', () => {
66
+ const ledger = newIngestLedger()
67
+ recordIngest(ledger, 1, undefined, T0)
68
+ recordVocabulary(ledger, 1)
69
+
70
+ assert.equal(ledger.window.rows, 0, '어휘가 저널 수에 섞였다')
71
+ assert.equal(ledger.window.vocabulary, 1)
72
+ })
73
+
74
+ test('★ 창이 갈리면 각 창이 자기 어휘를 든다 — 누적으로 비교하면 안 된다', () => {
75
+ /*
76
+ * 인티그레이션 레인이 짚은 것: `mes-line-a` 가 커서 48 · 저널 11 인데 결함이 아니다(그 사이
77
+ * 저널을 비웠다). **창 단위로** 맞춰야 한다.
78
+ */
79
+ const ledger = newIngestLedger()
80
+ recordIngest(ledger, 5, undefined, T0)
81
+ recordVocabulary(ledger, 2)
82
+ assert.equal(ledger.window.vocabulary, 2)
83
+
84
+ /* 창을 넘긴다 — 새 창은 0에서 시작한다. */
85
+ recordIngest(ledger, 5, undefined, T0 + 700_000)
86
+ assert.equal(ledger.window.vocabulary, 0, '새 창이 앞 창의 어휘를 물고 왔다')
87
+ assert.equal(ledger.vocabulary, 2, '누적은 남아야 한다')
88
+ })
89
+
90
+ test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
91
+ const ledger = newIngestLedger()
92
+ assert.equal(ledger.window.vocabulary, 0, '새 창에 그 항이 없다')
93
+ recordVocabulary(ledger, 0)
94
+ assert.equal(ledger.vocabulary, 0, '0을 세었다 — 모르면 안 적는다')
95
+ recordVocabulary(ledger, -3)
96
+ assert.equal(ledger.vocabulary, 0, '음수를 세었다')
97
+ })
@@ -0,0 +1,100 @@
1
+ /*
2
+ * 트윈의 고정 유입 규칙이 **표준 헤더 필드를 세 갈래 모두에서** 통과시키나.
3
+ *
4
+ * ── 왜 필요한가 (2026-09-07) ────────────────────────────────────────────────
5
+ * 같은 결함이 이 저장소에서 네 번 났고, 네 번 다 **층이 달랐다.**
6
+ *
7
+ * ```
8
+ * 1 EpcisHeaderOptions 에 칸이 없다 objectEvent 만 자기 인자로 갖고 있었다
9
+ * 2 매핑 스펙에 칸이 없다 커넥터가 선언할 자리가 없었다
10
+ * 3 이 파일의 규칙 셋에 칸이 없다 선언해도 여기서 사라졌다
11
+ * 4 받는 쪽 — 이건 처음부터 서 있었다
12
+ * ```
13
+ *
14
+ * 세 층이 각각 새는 자리였고, **아래 두 층을 고친 뒤에도 위층에서 계속 사라졌다.** 계약을 열고
15
+ * 「됐다」고 한 것이 아니라 인티그레이션 레인이 이 규칙 그대로 `mapRecord` 를 돌려 봐서 찾았다.
16
+ *
17
+ * 그래서 이 시험은 **계약이 아니라 이 파일의 규칙**을 돌린다. 계약 쪽 시험이 초록이어도 여기서
18
+ * 사라질 수 있다는 것이 오늘 실제로 일어난 일이다.
19
+ */
20
+ import test from 'node:test'
21
+ import assert from 'node:assert/strict'
22
+
23
+ import { mapRecordChecked } from '@operato/ops-contract'
24
+
25
+ import { canonicalRulesForTest } from '../server/engine/canonical-ingest.js'
26
+
27
+ const AT = '2026-09-07T02:00:00.000Z'
28
+
29
+ /** 세 갈래가 각자 성립하는 최소 레코드 — 헤더 값은 같은 것을 싣는다. */
30
+ const HEADER = {
31
+ bizStep: 'urn:epcglobal:cbv:bizstep:storing',
32
+ disposition: 'urn:epcglobal:cbv:disp:in_progress',
33
+ readPoint: 'urn:epc:id:sgln:0614141.00001.0',
34
+ bizLocation: 'urn:epc:id:sgln:0614141.00002.0',
35
+ bizTransactionList: [{ type: 'urn:epcglobal:cbv:btt:po', bizTransaction: 'urn:epcglobal:cbv:bt:0614141:order-33' }],
36
+ errorDeclaration: { declarationTime: AT, reason: 'urn:epcglobal:cbv:er:incorrect_data' }
37
+ }
38
+
39
+ const RECORDS: { sourceType: string; record: Record<string, unknown> }[] = [
40
+ { sourceType: 'twin', record: { ...HEADER, action: 'OBSERVE', epc: 'urn:epc:id:sgtin:0614141.107346.2018' } },
41
+ {
42
+ sourceType: 'twin-transformation',
43
+ record: {
44
+ ...HEADER,
45
+ inputEPCList: ['urn:epc:id:sgtin:0614141.107346.1000'],
46
+ outputEPCList: ['urn:epc:id:sgtin:0614141.107346.2000'],
47
+ transformationID: 'urn:uuid:11111111-2222-3333-4444-555555555555'
48
+ }
49
+ },
50
+ {
51
+ sourceType: 'twin-aggregation',
52
+ record: { ...HEADER, action: 'ADD', parentID: 'urn:epc:id:sscc:0614141.1234567890', childEPCs: ['urn:epc:id:sgtin:0614141.107346.2018'] }
53
+ }
54
+ ]
55
+
56
+ function mapWith(sourceType: string, record: Record<string, unknown>): any {
57
+ const rule = canonicalRulesForTest().find(r => r.sourceType === sourceType)
58
+
59
+ assert.ok(rule, `${sourceType} 규칙이 없다 — 갈래가 사라졌다`)
60
+
61
+ return mapRecordChecked(record as any, rule!.mapping as any, AT)
62
+ }
63
+
64
+ test('규칙이 셋이다 — 갈래가 줄면 그 사실이 조용히 사라진다', () => {
65
+ assert.deepEqual(
66
+ canonicalRulesForTest().map(r => r.sourceType).sort(),
67
+ ['twin', 'twin-aggregation', 'twin-transformation']
68
+ )
69
+ })
70
+
71
+ for (const { sourceType, record } of RECORDS) {
72
+ test(`${sourceType} 이 오더 참조를 커널까지 넘긴다 — 이 파일에서 사라지던 자리다`, () => {
73
+ const { event, errors } = mapWith(sourceType, record)
74
+
75
+ assert.deepEqual(errors, [], `${sourceType}: 성립하는 레코드인데 오류가 났다`)
76
+ assert.deepEqual(
77
+ event.bizTransactionList,
78
+ HEADER.bizTransactionList,
79
+ `${sourceType}: 오더 참조가 사라졌다 — 커넥터가 실어도 저널의 biz_transaction 이 빈다`
80
+ )
81
+ })
82
+
83
+ test(`${sourceType} 이 나머지 헤더 필드도 함께 넘긴다 — 하나만 보면 다음 것이 또 샌다`, () => {
84
+ const { event } = mapWith(sourceType, record)
85
+
86
+ assert.equal(event.bizStep, HEADER.bizStep)
87
+ assert.equal(event.disposition, HEADER.disposition)
88
+ assert.equal(event.readPoint?.id, HEADER.readPoint)
89
+ assert.equal(event.bizLocation?.id, HEADER.bizLocation)
90
+ assert.equal(event.errorDeclaration?.declarationTime, AT)
91
+ })
92
+ }
93
+
94
+ test('오더 참조를 안 실으면 그 칸을 만들지 않는다 — 빈 배열은 「참조가 없다」와 다르다', () => {
95
+ const { bizTransactionList: _drop, ...bare } = RECORDS[0].record
96
+ const { event, errors } = mapWith('twin', bare)
97
+
98
+ assert.deepEqual(errors, [])
99
+ assert.equal('bizTransactionList' in event, false)
100
+ })
@@ -0,0 +1,72 @@
1
+ /*
2
+ * ★ **보는 사람이 없어도 창이 닫힌다** — 아무도 안 보는 트윈일수록 기록이 없었다.
3
+ *
4
+ * ── 실측 (2026-09-05) ──────────────────────────────────────────────────────
5
+ * mes-miratek 사실 46건 · 62분에 흩어짐 창 1개
6
+ * mes-line-a 사실 11건 창 48개
7
+ *
8
+ * **사실 수와 창 수가 거꾸로다.** 창은 「다음 사건이 왔을 때」와 「누가 물어볼 때」만 굴렀고,
9
+ * `ingestHealthOf()` 가 굴리는 쪽이 실제로 창을 만들고 있었다 — 점검표·화면이 계속 묻는 트윈만
10
+ * 기록이 쌓였다.
11
+ *
12
+ * 그래서 **아무도 안 보는 트윈일수록 기록이 없었다.** 사실이 조용히 사라지는 것을 잡으려고 만든
13
+ * 장부가 정확히 그 반대로 돌았다. 실제로 137봉투가 장부 없이 지나갔다(인티그레이션 레인 실측).
14
+ *
15
+ * ── 왜 주기인가 ────────────────────────────────────────────────────────────
16
+ * 창이 닫히는 이유는 **시간이 지난 것**이다. 사건도 질의도 그것을 알려 주는 신호일 뿐이고 둘 다
17
+ * 안 올 수 있다. 시간은 늘 지나므로 주기가 그 사실에 맞는 신호다.
18
+ */
19
+ import { test } from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+
22
+ import { INGEST_WINDOW_MS, newIngestLedger, recordJournalWrite, rollIngestWindow } from '../server/engine/ingest-health.js'
23
+
24
+ const T0 = 1_757_000_000_000
25
+
26
+ test('★ 사건도 질의도 없이 시간만 지나면 창이 닫힌다', () => {
27
+ const ledger = newIngestLedger()
28
+ const closed: any[] = []
29
+
30
+ /* 사실 하나가 들어와 창이 열린다. */
31
+ recordJournalWrite(ledger, 1, T0, w => closed.push(w))
32
+ assert.equal(closed.length, 0, '열리자마자 닫히면 안 된다')
33
+
34
+ /* 그 뒤로 아무 일도 없다 — 주기만 온다. */
35
+ rollIngestWindow(ledger, T0 + INGEST_WINDOW_MS + 1, INGEST_WINDOW_MS, w => closed.push(w))
36
+
37
+ assert.equal(closed.length, 1, '시간이 지났는데 창이 안 닫혔다 — 기록이 안 남는다')
38
+ assert.equal(closed[0].rows, 1, '닫힌 창이 그 사실을 안 들었다')
39
+ })
40
+
41
+ test('★ 창 길이 전에는 안 닫는다 — 주기가 창을 잘게 쪼개면 안 된다', () => {
42
+ const ledger = newIngestLedger()
43
+ const closed: any[] = []
44
+ recordJournalWrite(ledger, 1, T0)
45
+
46
+ /* 주기(30초)가 창 길이(10분)보다 짧다 — 여러 번 와도 안 닫혀야 한다. */
47
+ for (let i = 1; i <= 10; i++) rollIngestWindow(ledger, T0 + i * 30_000, INGEST_WINDOW_MS, w => closed.push(w))
48
+
49
+ assert.equal(closed.length, 0, `주기가 창을 ${closed.length}개로 쪼갰다`)
50
+ })
51
+
52
+ test('★ 조용한 구간도 창으로 남는다 — expected-quiet 판정이 그 기록 위에 선다', () => {
53
+ /*
54
+ * 사실이 하나도 안 온 구간이 표에 남아야 「조용한 것이 정상인가」를 판정할 수 있다. 그 구간이
55
+ * 아예 기록되지 않으면 조용함과 기록 없음이 구별되지 않는다.
56
+ */
57
+ const ledger = newIngestLedger()
58
+ const closed: any[] = []
59
+ recordJournalWrite(ledger, 1, T0, w => closed.push(w))
60
+
61
+ /* 두 창을 통째로 건너뛴다 — 그 사이 아무 사실도 없었다. */
62
+ rollIngestWindow(ledger, T0 + INGEST_WINDOW_MS * 2 + 1, INGEST_WINDOW_MS, w => closed.push(w))
63
+
64
+ assert.ok(closed.length >= 1, '건너뛴 구간이 하나도 안 남았다')
65
+ })
66
+
67
+ test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
68
+ assert.ok(INGEST_WINDOW_MS > 0, `창 길이가 ${INGEST_WINDOW_MS} 다`)
69
+ const ledger = newIngestLedger()
70
+ recordJournalWrite(ledger, 3, T0)
71
+ assert.equal(ledger.window.rows, 3, '장부가 사실을 안 세고 있다 — 이 검사가 아무것도 안 잰다')
72
+ })
@@ -0,0 +1,175 @@
1
+ /*
2
+ * ★ **접수 타입에 있는 칸은 매핑도 옮긴다** — 문턱에서 조용히 버려지던 자리.
3
+ *
4
+ * ── 오늘 이 부류가 일곱 번이었다 (2026-09-04) ──────────────────────────────
5
+ * ActuationAck 이 세 칸을 버린다고 말한다 타입과 값 사이
6
+ * save 가 새 칸을 안 쓴다 칸과 쓰는 길
7
+ * 질의가 noteNext 를 안 낸다 저장과 화면 사이
8
+ * 커넥터가 등급 셋을 읽어 놓고 버린다 원본 쪽
9
+ * homeLocation vs homeLocationId 낱말 하나
10
+ * equipmentClassIds 칸이 접수면에 없다 ← 이 검사가 막는 것
11
+ * materialActual 안쪽을 안 본다 검사가 얕다
12
+ *
13
+ * **일곱 다 오류가 안 나서** 검사로만 잡혔다.
14
+ *
15
+ * ── 이 자리가 특히 조용하다 ─────────────────────────────────────────────────
16
+ * 접수 타입(`RefEquipment` …)에 칸이 있어도 매핑 함수가 안 옮기면 **원본이 채운 값이 문턱에서
17
+ * 사라진다.** 타입만 봐서는 안 보인다 — 타입은 「받는다」고 말하고 있으니까.
18
+ *
19
+ * 실제로 그렇게 났다: 설비 22대에 등급을 다 채웠는데 등급 목록만 서고 소속은 아무도 없었다.
20
+ * 화면에서 등급을 골라도 설비가 안 나왔고, 거절도 경고도 없었다.
21
+ *
22
+ * ── 왜 원문을 읽나 ──────────────────────────────────────────────────────────
23
+ * 매핑은 객체 리터럴이라 런타임에 「무엇을 옮기나」를 물을 수 없다(입력을 다 채워 넣어 답을 보는
24
+ * 방법이 있지만, 그러면 선택 필드마다 픽스처를 손으로 적어야 하고 그 손이 바로 빠뜨리는 자리다).
25
+ * 원문에서 읽으면 **이름을 손으로 적지 않아** 타입이 늘 때 검사가 함께 따라간다.
26
+ */
27
+ import { test } from 'node:test'
28
+ import assert from 'node:assert/strict'
29
+ import { readFileSync } from 'node:fs'
30
+
31
+ const SRC = readFileSync(new URL('../server/service/reference/reference-master.ts', import.meta.url), 'utf8')
32
+ .replace(/\/\*[\s\S]*?\*\//g, '')
33
+ .replace(/\/\/[^\n]*/g, '')
34
+
35
+ /** 접수 타입 하나의 칸 이름들 — `export interface X { … }` 의 깊이 1 필드. */
36
+ function intakeFields(name: string): string[] {
37
+ const start = SRC.indexOf(`export interface ${name} {`)
38
+ assert.ok(start >= 0, `접수 타입을 못 찾았다: ${name} — 이 검사가 따라가야 한다`)
39
+ const body = SRC.slice(SRC.indexOf('{', start))
40
+ const end = body.indexOf('\n}')
41
+ assert.ok(end > 0, `${name} 의 끝을 못 찾았다`)
42
+ return [...new Set([...body.slice(0, end).matchAll(/^\s{2}([a-zA-Z][\w]*)\??:/gm)].map(m => m[1]))]
43
+ }
44
+
45
+ /**
46
+ * 매핑 함수가 **읽는** 칸들 — `e.foo` · `a.foo` 처럼 원본 줄에서 꺼내는 이름.
47
+ *
48
+ * 옮긴 뒤 이름이 바뀌는 것도 있다(`homeLocationId → homeLocation`). 그래서 「나가는 이름」이 아니라
49
+ * **「읽는 이름」**을 센다 — 읽지 않으면 옮길 수도 없다.
50
+ */
51
+ function readFields(varName: string): string[] {
52
+ return [...new Set([...SRC.matchAll(new RegExp(`\\b${varName}\\.([a-zA-Z][\\w]*)`, 'g'))].map(m => m[1]))]
53
+ }
54
+
55
+ /**
56
+ * 일부러 안 옮기는 칸 — 이유와 함께.
57
+ *
58
+ * 비어 있는 것이 정상이 아니다: 접수면이 받아 두고 커널에 안 넘기는 값이 있으면 **왜 받는지**가
59
+ * 적혀 있어야 한다. 안 적으면 다음 사람이 「원래 그런가 보다」로 넘긴다.
60
+ */
61
+ const NOT_MAPPED: Record<string, { fields: string[]; why: string }> = {
62
+ RefEquipment: {
63
+ fields: [],
64
+ why: ''
65
+ },
66
+ RefAsset: {
67
+ fields: [],
68
+ why: ''
69
+ },
70
+ RefPerson: {
71
+ fields: [],
72
+ why: ''
73
+ }
74
+ }
75
+
76
+ const CASES = [
77
+ { intake: 'RefEquipment', reads: 'e' },
78
+ { intake: 'RefAsset', reads: 'a' },
79
+ { intake: 'RefPerson', reads: 'p' }
80
+ ] as const
81
+
82
+ test('★ 접수 타입의 칸을 매핑이 다 읽는다 — 안 읽으면 원본이 채운 값이 문턱에서 사라진다', () => {
83
+ const dropped: string[] = []
84
+
85
+ for (const c of CASES) {
86
+ const excused = new Set(NOT_MAPPED[c.intake]?.fields ?? [])
87
+ const read = new Set(readFields(c.reads))
88
+
89
+ for (const f of intakeFields(c.intake)) {
90
+ if (!read.has(f) && !excused.has(f)) dropped.push(`${c.intake}.${f}`)
91
+ }
92
+ }
93
+
94
+ assert.deepEqual(
95
+ dropped,
96
+ [],
97
+ '접수 타입은 받는다고 말하는데 매핑이 안 읽는다. 원본이 채운 값이 조용히 사라진다:\n ' +
98
+ dropped.join('\n ') +
99
+ '\n매핑에 넣거나, 일부러 안 넘긴다면 NOT_MAPPED 에 이유를 적는다.'
100
+ )
101
+ })
102
+
103
+ test('★ 등급 소속 셋이 다 접수면을 지난다 — 이름으로 못 박는다', () => {
104
+ /*
105
+ * 위 검사는 목록 대조라 예외 표에 올리면 통과한다. 이 셋은 **소속이 없으면 등급으로 부르는
106
+ * 요구가 채워지지 않는** 자리여서 예외가 될 수 없다.
107
+ */
108
+ for (const [type, field, reads] of [
109
+ ['RefEquipment', 'equipmentClassIds', 'e'],
110
+ ['RefAsset', 'assetClassIds', 'a'],
111
+ ['RefPerson', 'personnelClassIds', 'p']
112
+ ] as const) {
113
+ assert.ok(intakeFields(type).includes(field), `${type} 에 ${field} 칸이 없다`)
114
+ assert.ok(readFields(reads).includes(field), `매핑이 ${type}.${field} 를 안 읽는다 — 소속이 문턱에서 사라진다`)
115
+ }
116
+ })
117
+
118
+ test('★ 사람도 정체성 자리를 갖는다 — 그리고 그 자리는 개인정보가 아니다', () => {
119
+ /*
120
+ * ── 왜 열었나 (2026-09-05, 인티그레이션 레인이 물음) ────────────────────────
121
+ * 설비에서 이 자리를 만든 이유가 사람에게도 그대로 걸린다 — 지역 이름은 그 현장 안에서만
122
+ * 고유하다. `E-1001` 이 다른 공장에도 있으면 여러 현장을 함께 보는 순간 한쪽이 사라진다.
123
+ *
124
+ * ── 왜 이 검사에 규약을 함께 적나 ──────────────────────────────────────────
125
+ * 설비와 사람이 갈리는 자리가 있다. 사람의 「전역에서 고유한 값」은 사원번호·카드번호처럼
126
+ * **그 사람에 관한 사실**이고, 저널은 되풀이 읽히고 미러로 복제되고 구독으로 방송된다 —
127
+ * 한 번 들어가면 지워지지 않는다.
128
+ *
129
+ * 이 칸이 요구하는 것은 **현장을 앞에 붙여 전역에서 고유해진 이름**이다(`objectUri` 가 만든다).
130
+ * 그 규약이 주석에만 있으면 다음 커넥터가 카드번호를 넣는다 — 그러니 소스에 그 문장이 실제로
131
+ * 있는지를 검사가 지킨다.
132
+ */
133
+ assert.ok(intakeFields('RefPerson').includes('identity'), 'RefPerson 에 identity 칸이 없다')
134
+ assert.ok(readFields('p').includes('identity'), '매핑이 사람의 identity 를 안 읽는다')
135
+
136
+ /* 규약이 글로 남아 있나 — 지우면 이 줄이 빨개진다. */
137
+ const raw = readFileSync(new URL('../server/service/reference/reference-master.ts', import.meta.url), 'utf8')
138
+ const start = raw.indexOf('export interface RefPerson')
139
+ const personDoc = raw.slice(start, raw.indexOf('\n}', start))
140
+ assert.ok(personDoc.includes('개인정보'), '개인정보를 넣지 않는다는 규약이 사라졌다')
141
+ assert.ok(personDoc.includes('objectUri'), '무엇을 넣어야 하는지(이름공간 아래 식별자)가 사라졌다')
142
+ })
143
+
144
+ test('★ 정체성 낱말이 세 자리에서 같다 — 마스터가 내는 이름과 접수면이 받는 이름', () => {
145
+ /*
146
+ * 설비는 `identity` 로 바꿨는데 자산은 `gs1Id` 로 남아 있었다. 마스터는 둘 다 `identity` 로
147
+ * 내므로 자산 쪽은 **원본이 채우는 순간 조용히 사라졌다** — 값이 다 비어 있어 아직 잃은 것은
148
+ * 없었다(인티그레이션 레인 실측).
149
+ *
150
+ * 자리(`RefLocation`)는 `gs1Id` 가 맞다 — 거기는 실제로 SGLN 이 온다.
151
+ */
152
+ assert.ok(intakeFields('RefEquipment').includes('identity'), '설비 정체성 이름이 바뀌었다')
153
+ assert.ok(intakeFields('RefAsset').includes('identity'), '자산이 아직 마스터와 다른 이름을 쓴다')
154
+ assert.ok(!intakeFields('RefAsset').includes('gs1Id'), '자산에 옛 이름이 남아 있다')
155
+ assert.ok(intakeFields('RefLocation').includes('gs1Id'), '자리는 SGLN 이 오므로 그 이름이 맞다')
156
+ })
157
+
158
+ test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면 빨강', () => {
159
+ /*
160
+ * 정규식이 모양을 못 따라가면 목록이 비고, 그러면 위 검사가 **초록인데 아무것도 안 재는 상태**가
161
+ * 된다. 오늘 이 저장소에서 그 부류를 여러 번 잡았다.
162
+ */
163
+ for (const c of CASES) {
164
+ const fields = intakeFields(c.intake)
165
+ const read = readFields(c.reads)
166
+ assert.ok(fields.length > 5, `${c.intake} 칸을 ${fields.length}개만 읽었다`)
167
+ assert.ok(read.length > 5, `${c.reads}.* 를 ${read.length}개만 읽었다`)
168
+ }
169
+
170
+ /* 이유 없는 예외는 예외가 아니다 — 비어 있는 항목은 예외가 아니라 자리표시다. */
171
+ for (const [type, e] of Object.entries(NOT_MAPPED)) {
172
+ if (!e.fields.length) continue
173
+ assert.ok(e.why?.trim(), `${type} 의 예외에 이유가 없다`)
174
+ }
175
+ })
@@ -0,0 +1,170 @@
1
+ /*
2
+ * ★ **원천마다 시간의 결이 다르다** — 밤마다 나던 거짓 빨강의 나머지 반쪽.
3
+ *
4
+ * 판정은 이미 두 값을 받을 수 있다(`syncVerdictOf` 의 `expectedSilenceMs`·`quietNow`). 없던 것은
5
+ * **원천이 그것을 말할 자리**였다. 인티그레이션 레인이 다섯을 재 왔다.
6
+ *
7
+ * ppms 발전 일몰에 멈춘다 — 두 사이트가 같은 분 19:25 에 함께
8
+ * ppms 계측 밤에도 온다 · 15분마다
9
+ * ppms 가격 화·목만 거래된다 · 그 날 하루 한 번
10
+ * plant 근무시간 창 · 훅이라 「보내면 온다」
11
+ * chef 조업시간 창 · 같음
12
+ *
13
+ * 셋이 다 다르다 — 해에 매인 것, 사람 근무에 매인 것, 거래일에 매인 것. 그런데 **두 축으로
14
+ * 덮인다**: 「지금이 활동 시각인가」와 「그 안에서 얼마나 뜸한가」.
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+
19
+ import { cadenceOf, ingestExpectation, type LiveCadence } from '../server/service/reference/live-cadence.js'
20
+
21
+ const NOW = 1_800_000_000_000
22
+
23
+ /* ── 축을 찾는다 ──────────────────────────────────────────────────────────── */
24
+
25
+ test('★ 선언한 축을 찾는다', () => {
26
+ const cadences: LiveCadence[] = [
27
+ { axis: 'equipment.status', windowKind: 'daylight' },
28
+ { axis: 'quality.output', expectedSilenceMs: 900_000 }
29
+ ]
30
+
31
+ assert.equal(cadenceOf(cadences, 'quality.output')?.expectedSilenceMs, 900_000)
32
+ assert.equal(cadenceOf(cadences, 'equipment.status')?.windowKind, 'daylight')
33
+ })
34
+
35
+ test('★ 선언하지 않은 축은 없다고 답한다 — 옛 거동으로 견주게', () => {
36
+ assert.equal(cadenceOf([{ axis: 'quality.output' }], 'order.status'), undefined)
37
+ assert.equal(cadenceOf(undefined, 'quality.output'), undefined)
38
+ })
39
+
40
+ /* ── 판정에 넘길 두 값으로 옮긴다 ───────────────────────────────────────── */
41
+
42
+ test('★ 선언이 없으면 아무것도 답하지 않는다 — 「모른다」를 「안 와도 된다」로 접지 않는다', () => {
43
+ /*
44
+ * 여기서 `quietNow: true` 를 내면 **선언 안 한 원천이 전부 조용해진다.** 정말 끊긴 것도
45
+ * 조용해지고, 그러면 이 축을 만든 목적을 잃는다.
46
+ */
47
+ assert.deepEqual(ingestExpectation(undefined, { nowMs: NOW }), {})
48
+ })
49
+
50
+ test('★ 15분마다 긷는 원천 — 주기만 넘긴다', () => {
51
+ const out = ingestExpectation({ axis: 'quality.output', expectedSilenceMs: 900_000 }, { nowMs: NOW })
52
+
53
+ assert.equal(out.expectedSilenceMs, 900_000)
54
+ assert.equal(out.quietNow, undefined, '창을 선언하지 않았는데 시각을 단언했다')
55
+ })
56
+
57
+ test('0이나 음수 주기는 선언이 아니다', () => {
58
+ for (const bad of [0, -1, NaN]) {
59
+ const out = ingestExpectation({ axis: 'a', expectedSilenceMs: bad }, { nowMs: NOW })
60
+ assert.equal(out.expectedSilenceMs, undefined, `${bad} 를 주기로 받았다`)
61
+ }
62
+ })
63
+
64
+ /* ── 「보내면 온다」 ──────────────────────────────────────────────────────── */
65
+
66
+ test('★ 훅으로 미는 축은 침묵으로 판정하지 않는다 — plant 가 그렇다', () => {
67
+ /*
68
+ * 보낼 것이 없으면 안 보내는 것이 정상이다. 「주기가 없다」와 「모른다」를 가르는 값이다.
69
+ */
70
+ const out = ingestExpectation({ axis: 'order.status', pushed: true }, { nowMs: NOW })
71
+
72
+ assert.equal(out.quietNow, true)
73
+ })
74
+
75
+ test('★ 미는 축은 창 판정을 기다리지 않는다 — 판정 수단이 없어도 답한다', () => {
76
+ const out = ingestExpectation(
77
+ { axis: 'order.status', pushed: true, window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
78
+ { nowMs: NOW } /* inWindow 를 주지 않았다 */
79
+ )
80
+
81
+ assert.equal(out.quietNow, true)
82
+ })
83
+
84
+ /* ── 창 ──────────────────────────────────────────────────────────────────── */
85
+
86
+ test('★ 활동 시각 안이면 조용할 때가 아니다', () => {
87
+ const out = ingestExpectation(
88
+ { axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
89
+ { nowMs: NOW, inWindow: () => true }
90
+ )
91
+
92
+ assert.equal(out.quietNow, false)
93
+ })
94
+
95
+ test('★ 활동 시각 밖이면 조용할 때다', () => {
96
+ const out = ingestExpectation(
97
+ { axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
98
+ { nowMs: NOW, inWindow: () => false }
99
+ )
100
+
101
+ assert.equal(out.quietNow, true)
102
+ })
103
+
104
+ test('★ 판정 수단을 안 받았으면 시각을 단언하지 않는다 — 지어낸 값을 만들지 않는다', () => {
105
+ /*
106
+ * 부르는 쪽이 달력 판정 함수를 주지 않았는데 우리가 「활동 시각이다」로 답하면, 창을 선언한
107
+ * 원천이 **창이 없는 것과 같아진다.** 그것은 조용한 실패다.
108
+ */
109
+ const out = ingestExpectation(
110
+ { axis: 'quality.output', window: [{ id: 'day', entryType: 'production', startTime: '09:00', endTime: '18:00' }] },
111
+ { nowMs: NOW }
112
+ )
113
+
114
+ assert.equal(out.quietNow, undefined)
115
+ })
116
+
117
+ test('빈 창은 창이 아니다', () => {
118
+ assert.equal(ingestExpectation({ axis: 'a', window: [] }, { nowMs: NOW, inWindow: () => false }).quietNow, undefined)
119
+ })
120
+
121
+ /* ── 두 축이 함께 온다 ──────────────────────────────────────────────────── */
122
+
123
+ test('★ 주기와 창을 같이 선언할 수 있다 — 인증서 가격이 그 모양이다', () => {
124
+ /*
125
+ * 화·목만 거래되고 그 날 하루 한 번 온다. 「매일 와야 한다」로 재면 주 5일 중 3일이 빨갛고,
126
+ * 「주 2회」로 재면 이어 쓴 날을 못 본다. 두 축이면 둘 다 답한다.
127
+ */
128
+ const out = ingestExpectation(
129
+ {
130
+ axis: 'quality.output',
131
+ expectedSilenceMs: 26 * 3_600_000,
132
+ window: [
133
+ { id: 'tue', entryType: 'production', startTime: '00:00', endTime: '23:59' },
134
+ { id: 'thu', entryType: 'production', startTime: '00:00', endTime: '23:59' }
135
+ ]
136
+ },
137
+ { nowMs: NOW, inWindow: () => false }
138
+ )
139
+
140
+ assert.equal(out.expectedSilenceMs, 26 * 3_600_000)
141
+ assert.equal(out.quietNow, true, '거래일이 아니면 조용한 것이 정상이다')
142
+ })
143
+
144
+ /* ── 「해 있는 동안」은 호스트가 채운다 ──────────────────────────────────── */
145
+
146
+ test('★ 종류만 선언한 것으로는 창 판정을 하지 않는다 — 호스트가 채워야 한다', () => {
147
+ /*
148
+ * 커널은 일출·일몰을 계산하지 않는다(굴절·고위도·박명 정의가 다 선택이고, 정하면 방언이다).
149
+ * 원천은 종류만 말하고, 좌표를 아는 호스트가 날마다 구체적인 구간을 채운다.
150
+ *
151
+ * 그러니 `windowKind` 만 있고 `window` 가 비어 있으면 **아직 채워지지 않은 것**이고, 그때
152
+ * 판정하지 않는다. `false` 로 접으면 태양광이 밤에도 활동 시각인 것이 된다.
153
+ */
154
+ const out = ingestExpectation({ axis: 'equipment.status', windowKind: 'daylight' }, { nowMs: NOW, inWindow: () => true })
155
+
156
+ assert.equal(out.quietNow, undefined)
157
+ })
158
+
159
+ test('★ 호스트가 채우면 그것으로 판정한다', () => {
160
+ const out = ingestExpectation(
161
+ {
162
+ axis: 'equipment.status',
163
+ windowKind: 'daylight',
164
+ window: [{ id: 'today', entryType: 'production', startTime: '06:12', endTime: '19:25' }]
165
+ },
166
+ { nowMs: NOW, inWindow: () => false }
167
+ )
168
+
169
+ assert.equal(out.quietNow, true)
170
+ })