@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,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
+ })
@@ -0,0 +1,63 @@
1
+ /*
2
+ * The name a live feed takes its lease under.
3
+ *
4
+ * This is worth a test because the failure it prevents is silent: two feeds
5
+ * sharing one name means one of them stops, and it stops looking like
6
+ * "someone else holds it" — the ordinary case on every instance but one, so
7
+ * nothing gets logged and nothing looks wrong.
8
+ */
9
+ import test from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+
12
+ import { liveFeedLeaseName } from '../server/service/reference/live-feed-lease.js'
13
+
14
+ test('the domain is part of the name, so two tenants do not block each other', () => {
15
+ /*
16
+ * This is the whole reason the reference layer composes the name instead of
17
+ * the adapter: an adapter is handed cfg and site, and neither carries a
18
+ * domain, so a name built there would collide across tenants.
19
+ */
20
+ const a = liveFeedLeaseName('domain-a', 'mes-line-a', 'mes')
21
+ const b = liveFeedLeaseName('domain-b', 'mes-line-a', 'mes')
22
+
23
+ assert.notEqual(a, b)
24
+ assert.ok(a.includes('domain-a'))
25
+ })
26
+
27
+ test('the instance is part of the name, so two twins on one source do not block each other', () => {
28
+ const a = liveFeedLeaseName('d', 'mes-line-a', 'mes')
29
+ const b = liveFeedLeaseName('d', 'mes-miratek', 'mes')
30
+
31
+ assert.notEqual(a, b)
32
+ })
33
+
34
+ test('the same feed always gets the same name, so a second instance sees it as taken', () => {
35
+ assert.equal(liveFeedLeaseName('d', 'i', 's'), liveFeedLeaseName('d', 'i', 's'))
36
+ })
37
+
38
+ test('the name says it is a live feed, so a lease table read tells you the kind of task', () => {
39
+ assert.ok(liveFeedLeaseName('d', 'i', 's').startsWith('twin-live::'))
40
+ })
41
+
42
+ test('a missing source still names the feed — an instance has one feed', () => {
43
+ const named = liveFeedLeaseName('d', 'i')
44
+
45
+ assert.equal(named, 'twin-live::d::i')
46
+ assert.notEqual(named, liveFeedLeaseName('d', 'other'))
47
+ })
48
+
49
+ test('a blank source is the same as none rather than an empty segment', () => {
50
+ assert.equal(liveFeedLeaseName('d', 'i', ' '), liveFeedLeaseName('d', 'i'))
51
+ })
52
+
53
+ test('a missing domain or instance is refused, not filled in', () => {
54
+ /*
55
+ * Composing a name from a blank would put two feeds on one row. Throwing
56
+ * here surfaces it at startup; filling it in would surface it as a channel
57
+ * that quietly stopped weeks later.
58
+ */
59
+ assert.throws(() => liveFeedLeaseName('', 'i', 's'), /domainId/)
60
+ assert.throws(() => liveFeedLeaseName(' ', 'i', 's'), /domainId/)
61
+ assert.throws(() => liveFeedLeaseName('d', '', 's'), /instanceId/)
62
+ assert.throws(() => liveFeedLeaseName('d', undefined as any, 's'), /instanceId/)
63
+ })
@@ -213,7 +213,7 @@ test('★ OEE derive: 공유 공식 computeOee 가 공급된 카운터로 OEE
213
213
  */
214
214
  const planned = o.runMs + o.setupMs + o.downMs + o.delayMs
215
215
  const derived = computeOee(
216
- { runMs: o.runMs, setupMs: o.setupMs, downMs: o.downMs, goodCount: o.goodCount, scrapCount: o.scrapCount, metricsSinceMs: 0 },
216
+ { runMs: o.runMs, setupMs: o.setupMs, downMs: o.downMs, goodCount: o.goodCount, nonconformingCount: o.nonconformingCount, metricsSinceMs: 0 },
217
217
  planned
218
218
  )
219
219
  /* 세 비율은 근거가 없으면 없다 — 양쪽 다 없는 것도 재생산이다(계약 0.5.0). */
@@ -90,13 +90,13 @@ test('★ OEE 누적기: quality.output 델타 소비 → quality 정확 (good/s
90
90
  const ev = (ms: number, type: string, data: any): any => ({ eventId: 'x', eventType: type, eventTime: new Date(T0 + ms).toISOString(), tenantId: 't', data })
91
91
 
92
92
  acc.apply(ev(0, 'equipment.status', { moverId: 'w1', status: 'busy' }))
93
- acc.apply(ev(1000, 'quality.output', { moverId: 'w1', good: false, goodCount: 0, scrapCount: 1 }))
94
- acc.apply(ev(2000, 'quality.output', { moverId: 'w1', good: true, goodCount: 8, scrapCount: 2 })) // 누적값 최신화
93
+ acc.apply(ev(1000, 'quality.output', { moverId: 'w1', good: false, goodCount: 0, nonconformingCount: 1 }))
94
+ acc.apply(ev(2000, 'quality.output', { moverId: 'w1', good: true, goodCount: 8, nonconformingCount: 2 })) // 누적값 최신화
95
95
  acc.apply(ev(3000, 'equipment.status', { moverId: 'w1', status: 'idle' }))
96
96
 
97
97
  const c = acc.counters('w1', T0 + 4000)
98
98
  assert.equal(c.goodCount, 8, 'goodCount 추적(누적 최신)')
99
- assert.equal(c.scrapCount, 2, 'scrapCount 추적')
99
+ assert.equal(c.nonconformingCount, 2, 'nonconformingCount 추적')
100
100
  const oee = acc.oee('w1', T0 + 4000)
101
101
  assert.ok(Math.abs(oee.quality - 0.8) < 1e-9, 'quality = good/(good+scrap) = 0.8 (텔레메트리 없이 이벤트로 도출)')
102
102
  })
@@ -0,0 +1,146 @@
1
+ /*
2
+ * 현장의 조업 달력이 계획 조업 시간에 닿나.
3
+ *
4
+ * ── 왜 필요한가 (2026-09-07) ────────────────────────────────────────────────
5
+ * 계획 조업 시간(PBT)이 「재기 시작한 뒤 흐른 시간 전부」였다. 주말도 야간도 휴일도 계획 조업으로
6
+ * 세었다. [산술] 8시간 × 주5일이면 168시간 중 40시간이 조업이니 PBT 가 4.2배 크다.
7
+ *
8
+ * plant 레인이 같은 자리를 먼저 겪고 실측을 냈다 — 3교대를 물려받으면 `availability 0.4583`,
9
+ * 달력이 아예 없으면 수를 안 낸다. 트윈은 창 전체를 계획으로 쳐서 수를 냈다.
10
+ *
11
+ * ── 이 시험이 지키는 것은 「자리가 있고 부르는 곳이 있다」다 ─────────────────
12
+ * 오늘 이 저장소에서 그것을 여섯 번 놓쳤다. 값을 받을 칸을 만들고 아무도 채우지 않거나, 채우고
13
+ * 아무도 안 읽는 자리다. 그래서 여기서는 **누적기가 그 값을 실제로 쓰는지**를 본다 — 달력을 얹은
14
+ * 것과 안 얹은 것의 답이 달라야 한다.
15
+ */
16
+ import test from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+
19
+ import { OeeAccumulator } from '../server/engine/oee-accumulator.js'
20
+
21
+ const HOUR = 3_600_000
22
+ const MON = Date.UTC(2026, 8, 7, 0, 0, 0)
23
+
24
+ /** 08:00~16:00 만 조업 — 하루 열여섯 시간이 비근무다. */
25
+ const EIGHT_HOURS = [{ id: 'day', fromTime: '08:00', toTime: '16:00' }] as any[]
26
+
27
+ /** 여덟 시간 내내 돈 설비 한 대. */
28
+ function ranAllShift(calendar?: any[]) {
29
+ const acc = new OeeAccumulator()
30
+ acc.setWorkCalendar(calendar ? { workCalendar: calendar } : undefined)
31
+ /*
32
+ * 봉투의 `data` 에 `moverId` 다 — `payload.data` 도 `equipmentId` 도 아니다. 처음 그렇게 써서
33
+ * 누적기가 아무것도 안 받았고, 시험 넷이 빨개져서 알았다. 읽어서 고쳤다(`oee-accumulator.ts:111`).
34
+ */
35
+ acc.apply({
36
+ eventType: 'equipment.status',
37
+ eventTime: new Date(MON + 8 * HOUR).toISOString(),
38
+ data: { moverId: 'EQ-1', status: 'busy' }
39
+ } as any)
40
+
41
+ return acc.counters('EQ-1', MON + 24 * HOUR)
42
+ }
43
+
44
+ test('달력을 얹으면 계획 정지가 나온다 — 계측 창 안의 비근무만', () => {
45
+ /*
46
+ * ⚠ 하루가 아니다. 계측 창은 **첫 이벤트부터**이므로 08:00~24:00 의 열여섯 시간이고, 그중 조업이
47
+ * 08:00~16:00 이니 비근무가 여덟 시간이다.
48
+ *
49
+ * 처음 이 값을 열여섯으로 적었다 — 「하루 중 열여섯 시간이 비근무」라는 달력의 성질을 계측 창에
50
+ * 그대로 옮긴 것이다. 재보고 고쳤다. 계획 정지는 **달력의 성질이 아니라 이 창에서 잰 값**이다.
51
+ */
52
+ const c = ranAllShift(EIGHT_HOURS) as any
53
+
54
+ assert.equal(c.holdMs, 8 * HOUR)
55
+ assert.equal(c.metricsSinceMs, MON + 8 * HOUR, '창이 첫 이벤트부터라는 것이 위 계산의 전제다')
56
+ })
57
+
58
+ test('달력이 없으면 그 칸을 만들지 않는다 — 0 은 「쉬는 시간이 없다」는 주장이다', () => {
59
+ const c = ranAllShift() as any
60
+
61
+ assert.equal('holdMs' in c, false)
62
+ })
63
+
64
+ test('얹은 것과 안 얹은 것의 답이 다르다 — 같으면 그 값이 안 쓰이는 것이다', () => {
65
+ /*
66
+ * 이 시험이 이 파일의 요점이다. 오늘 제가 낸 결함 중 하나가 「값을 넣어 줄 사람이 있는지 아무것도
67
+ * 검사하지 않은 시험」이었다 — 고쳐도 통과하고 안 고쳐도 통과했다.
68
+ */
69
+ const withCal = ranAllShift(EIGHT_HOURS) as any
70
+ const without = ranAllShift() as any
71
+
72
+ assert.notEqual(withCal.holdMs, without.holdMs)
73
+ })
74
+
75
+ test('실 생산 시간은 달력과 무관하다 — APT 는 잰 값이다', () => {
76
+ /*
77
+ * 달력은 PBT 를 정하고 APT 는 안 건드린다. 휴일에 돌린 열한 시간이 사라지던 것이 그 반대 방향의
78
+ * 결함이었고, 그것은 자르는 쪽이었다.
79
+ */
80
+ const withCal = ranAllShift(EIGHT_HOURS)
81
+ const without = ranAllShift()
82
+
83
+ assert.equal(withCal.runMs, without.runMs)
84
+ assert.equal(withCal.runMs, 16 * HOUR, '08:00 에 busy 로 들어가 24:00 까지면 열여섯 시간이다')
85
+ })
86
+
87
+ test('잰 구간의 시작은 그대로다 — 그 칸에 파생값을 얹지 않는다', () => {
88
+ /*
89
+ * 같은 효과를 `metricsSinceMs` 를 당겨서 낼 수도 있는데, 그 칸은 「언제부터 쟀나」라는 사실이다.
90
+ * plant 레인이 그 방식으로 하고 있었고 `holdMs` 로 옮겼다 — 수는 자릿수까지 같았다.
91
+ */
92
+ const c = ranAllShift(EIGHT_HOURS)
93
+
94
+ assert.equal(c.metricsSinceMs, MON + 8 * HOUR)
95
+ })
96
+
97
+ /*
98
+ * ── 설비의 달력이 현장의 것을 이긴다 ────────────────────────────────────────
99
+ * 이 자리가 실제 자료를 살렸다. rosarito 트윈들이 설비 41대에 3교대 × 평일 달력을 이미 갖고 있었는데,
100
+ * 처음 배선할 때 **현장 달력만 읽었다.** 현장에 달력을 선언한 공간이 하나도 없어서, 있는 자료를 두고
101
+ * 계획 조업 시간이 창 전체로 남아 있었다.
102
+ *
103
+ * 사용자가 「모든 공간에 달력을 세우라」고 한 것이 그 말을 듣고 나온 지시였고, 재보니 세울 것이 아니라
104
+ * **읽을 것이 있었다.**
105
+ */
106
+ const ROUND_CLOCK = [{ id: 'always', fromTime: '00:00', toTime: '24:00' }] as any[]
107
+
108
+ function withModel(model: any) {
109
+ const acc = new OeeAccumulator()
110
+ acc.setWorkCalendar(model)
111
+ acc.apply({
112
+ eventType: 'equipment.status',
113
+ eventTime: new Date(MON + 8 * HOUR).toISOString(),
114
+ data: { moverId: 'EQ-1', status: 'busy' }
115
+ } as any)
116
+
117
+ return acc.counters('EQ-1', MON + 24 * HOUR) as any
118
+ }
119
+
120
+ test('설비가 자기 달력을 선언하면 그것을 쓴다 — 24시간 도는 로가 현장 근무조에 안 잘린다', () => {
121
+ const c = withModel({ workCalendar: EIGHT_HOURS, equipment: [{ id: 'EQ-1', workCalendar: ROUND_CLOCK }] })
122
+
123
+ assert.equal(c.holdMs, 0, '자기 달력이 종일 가동이면 계획 정지가 없다')
124
+ })
125
+
126
+ test('설비가 선언하지 않으면 현장의 달력으로 떨어진다', () => {
127
+ const c = withModel({ workCalendar: EIGHT_HOURS, equipment: [{ id: 'EQ-OTHER', workCalendar: ROUND_CLOCK }] })
128
+
129
+ assert.equal(c.holdMs, 8 * HOUR, '이 설비의 것이 없으니 현장 것을 쓴다')
130
+ })
131
+
132
+ test('현장이 없고 설비만 있어도 답한다 — 지금 실제 자료가 그 모양이다', () => {
133
+ /*
134
+ * rosarito 가 이렇다: 공간에 달력이 없고 설비 41대에 3교대가 있다. 현장 달력만 읽으면 이 자료가
135
+ * 통째로 안 쓰인다.
136
+ */
137
+ const c = withModel({ equipment: [{ id: 'EQ-1', workCalendar: EIGHT_HOURS }] })
138
+
139
+ assert.equal(c.holdMs, 8 * HOUR)
140
+ })
141
+
142
+ test('둘 다 없으면 그 칸을 만들지 않는다', () => {
143
+ const c = withModel({ equipment: [{ id: 'EQ-1' }] })
144
+
145
+ assert.equal('holdMs' in c, false)
146
+ })