@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
@@ -20,11 +20,49 @@ class OeeAccumulator {
20
20
  constructor() {
21
21
  this.acc = new Map();
22
22
  this.maxMs = 0; // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)
23
+ this.calendarByResource = new Map();
24
+ }
25
+ /**
26
+ * 모델에서 조업 달력을 읽어 얹는다 — 모델을 들고 있는 쪽(`TwinEngine`)이 부른다.
27
+ *
28
+ * ── 우선순위가 여기 있는 이유 ───────────────────────────────────────────────
29
+ * ```
30
+ * 설비의 달력 이 설비의 가동 계획 — 선언했으면 이것이 이긴다 (24시간 도는 로)
31
+ * 현장의 달력 이 현장의 조업일
32
+ * 없음 제약 없음 — 계획 조업 시간이 창 전체가 된다
33
+ * ```
34
+ * 계획 조업 시간을 내는 자리가 여기라 그 규칙도 여기 둔다. 부르는 쪽마다 고르게 하면 라이브와
35
+ * 히스토리가 다른 답을 낸다.
36
+ *
37
+ * ⚠ **설비의 달력은 사람의 근무조가 아니다.** 씨앗이 사람의 근무조를 설비 달력에 복사한 일이 있었고,
38
+ * 그때 아무도 근무하지 않는 시간이 근무 시간으로 기록됐다. 사람 쪽은 `persons[].workCalendar` 다.
39
+ *
40
+ * 세우는 자리에서 한 번만 주지 않고 따로 부르는 이유는 모델이 다시 실릴 수 있기 때문이다
41
+ * (재동기화 · 이관). 그때 달력만 갈아 끼우면 누적된 계측을 버리지 않는다.
42
+ */
43
+ setWorkCalendar(model) {
44
+ this.siteCalendar = model?.workCalendar?.length ? model.workCalendar : undefined;
45
+ this.utcOffsetMinutes = model?.utcOffsetMinutes;
46
+ this.calendarByResource.clear();
47
+ for (const e of model?.equipment ?? []) {
48
+ const cal = e?.workCalendar;
49
+ if (e?.id && Array.isArray(cal) && cal.length)
50
+ this.calendarByResource.set(String(e.id), cal);
51
+ }
52
+ }
53
+ /**
54
+ * 이 설비의 계획 가동 달력 — 자기 것이 있으면 그것, 없으면 현장의 것.
55
+ *
56
+ * 인자 이름이 `moverId` 인 것은 **봉투의 옛 세대 필드 이름**이라 그렇다(§`apply`). 새로 만드는
57
+ * 이름에는 그 낱말을 쓰지 않는다 — 어휘 가드가 그것을 잡았고, 맞게 잡았다.
58
+ */
59
+ calendarFor(moverId) {
60
+ return this.calendarByResource.get(moverId) ?? this.siteCalendar;
23
61
  }
24
62
  ensure(moverId, t) {
25
63
  let a = this.acc.get(moverId);
26
64
  if (!a) {
27
- a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
65
+ a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0 };
28
66
  this.acc.set(moverId, a);
29
67
  }
30
68
  return a;
@@ -59,7 +97,7 @@ class OeeAccumulator {
59
97
  return;
60
98
  const a = this.ensure(q.moverId, t);
61
99
  a.goodCount = q.goodCount ?? a.goodCount; // 누적값 최신화(good/scrap 은 무버 누적)
62
- a.scrapCount = q.scrapCount ?? a.scrapCount;
100
+ a.nonconformingCount = q.nonconformingCount ?? a.nonconformingCount;
63
101
  return;
64
102
  }
65
103
  if (env.eventType !== 'equipment.status')
@@ -86,7 +124,7 @@ class OeeAccumulator {
86
124
  * 없음을 없음으로 다룬다(§computeOee).
87
125
  */
88
126
  if (!a)
89
- return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 };
127
+ return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0 };
90
128
  const dt = Math.max(0, nowMs - a.lastMs);
91
129
  let busyMs = a.busyMs;
92
130
  let downMs = a.downMs;
@@ -94,7 +132,21 @@ class OeeAccumulator {
94
132
  busyMs += dt;
95
133
  else if (a.status === 'down')
96
134
  downMs += dt;
97
- return { runMs: busyMs, setupMs: 0, downMs, goodCount: a.goodCount, scrapCount: a.scrapCount, metricsSinceMs: a.startMs };
135
+ /*
136
+ * 계획 정지 — 계약이 이 칸에 그 뜻을 적어 뒀다(점심 · 예방보전 · 교대). 휴일과 주말이 같은
137
+ * 갈래다. `metricsSinceMs` 를 당겨서 같은 효과를 내지 않는다: 그 칸은 「언제부터 쟀나」라는
138
+ * 사실이고, 파생값을 얹으면 실제보다 늦게 재기 시작했다고 말하는 것이 된다.
139
+ */
140
+ const holdMs = (0, ops_contract_1.nonWorkingMsIn)(this.calendarFor(moverId), a.startMs, nowMs, this.utcOffsetMinutes);
141
+ return {
142
+ runMs: busyMs,
143
+ setupMs: 0,
144
+ downMs,
145
+ goodCount: a.goodCount,
146
+ nonconformingCount: a.nonconformingCount,
147
+ metricsSinceMs: a.startMs,
148
+ ...(holdMs !== undefined ? { holdMs } : {})
149
+ };
98
150
  }
99
151
  /** 공유 공식으로 근사 OEE 산출 — 정확한 OEE 는 실 텔레메트리(option ①). nowMs 기본=최신 이벤트 시각. */
100
152
  oee(moverId, nowMs = this.maxMs) {
@@ -1 +1 @@
1
- {"version":3,"file":"oee-accumulator.js","sourceRoot":"","sources":["../../server/engine/oee-accumulator.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * OEE 이벤트 누적기 (순수·근사) — live-모드 OEE 입력 생산의 한 경로(face2-inbound-live §1.1, option ②).\n *\n * 문제: OEE 공식(computeOee)은 sim/live 공유되나, 입력 카운터(runMs/downMs/…)는 미러 State·이벤트에 없다\n * (emitEquipment 는 status/location/motion 만 방출). → live 는 카운터를 별도 생산해야 한다.\n * 이 누적기: equipment.status 전이 스트림으로 무버별 시간-in-상태를 누적해 카운터를 근사 생산.\n *\n * 정확: downMs(고장 시간 = down 구간 합) · quality(good/scrap = quality.output 델타). 근사·한계:\n * - busyMs 를 runMs 로(setup 분리 불가 — 이벤트에 setup 없음) → performance 낙관·availability 는 setup 손실 미반영.\n * - quality.output 이 없으면(예: WMS/YMS) quality=1(품질 개념 없음).\n * 시간축 = Date.parse(eventTime)(ms) — 구간 차분은 sim/실시간 무관하게 정확(고정 오프셋 상쇄).\n */\n/* OEE 공식은 계약에 있다 — 소비자가 셋이라(시뮬·미러·MES) 커널에 두면 밖에서 다시 쓰게 된다. */\nimport { computeOee } from '@operato/ops-contract'\nimport type { OeeCounters } from '@operato/ops-contract'\nimport type { CanonicalEnvelope, OeeMetrics } from '@operato/ops-contract'\n\ninterface Acc {\n status?: string\n lastMs: number\n startMs: number\n busyMs: number\n downMs: number\n goodCount: number\n scrapCount: number\n}\n\n/** 누적기의 완전한 재개점 — 저장했다 되살릴 수 있어야 꼬리만 계산할 수 있다. */\nexport interface OeeCheckpoint {\n maxMs: number\n acc: ({ moverId: string } & Acc)[]\n}\n\nexport class OeeAccumulator {\n private acc = new Map<string, Acc>()\n private maxMs = 0 // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)\n\n private ensure(moverId: string, t: number): Acc {\n let a = this.acc.get(moverId)\n if (!a) {\n a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 }\n this.acc.set(moverId, a)\n }\n return a\n }\n\n /**\n * **재개점** — 이 누적기의 상태 전부(이어 계산의 씨앗).\n *\n * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────\n * 가동 이력은 원 시스템이 누적을 주지 않아 **우리가 이벤트를 적분해** 만든다. 그래서 저널을 꼬리만\n * 계산하면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 알리지 않고 작아진다(오류는 나지 않는다).\n * 재개점을 함께 저장·복원해야 「이어 계산한 값」이 「0부터 계산한 값」과 같아진다.\n */\n serialize(): OeeCheckpoint {\n return {\n maxMs: this.maxMs,\n acc: [...this.acc.entries()].map(([moverId, a]) => ({ moverId, ...a }))\n }\n }\n\n /** 재개점에서 되세운다 — 모르는 것은 지어내지 않는다(없으면 빈 누적기다). */\n restore(cp: OeeCheckpoint | undefined | null): void {\n this.maxMs = cp?.maxMs ?? 0\n this.acc = new Map((cp?.acc ?? []).map(({ moverId, ...a }) => [moverId, { ...a }]))\n }\n\n /** equipment.status(시간-in-상태) + quality.output(양품/불량) 소비. 다른 eventType 무시. */\n apply(env: CanonicalEnvelope): void {\n const t = Date.parse(env.eventTime)\n if (t > this.maxMs) this.maxMs = t\n if (env.eventType === 'quality.output') {\n const q = env.data as { moverId?: string; goodCount?: number; scrapCount?: number }\n if (!q?.moverId) return\n const a = this.ensure(q.moverId, t)\n a.goodCount = q.goodCount ?? a.goodCount // 누적값 최신화(good/scrap 은 무버 누적)\n a.scrapCount = q.scrapCount ?? a.scrapCount\n return\n }\n if (env.eventType !== 'equipment.status') return\n const d = env.data as { moverId?: string; status?: string }\n if (!d?.moverId) return\n const a = this.ensure(d.moverId, t)\n const dt = Math.max(0, t - a.lastMs)\n if (a.status === 'busy') a.busyMs += dt\n else if (a.status === 'down') a.downMs += dt\n a.status = d.status\n a.lastMs = t\n }\n\n /** 무버 OEE 카운터 — 마지막 상태 구간을 nowMs(기본=최신 이벤트 시각)까지 flush. busy→run 근사, setup=0. */\n counters(moverId: string, nowMs: number = this.maxMs): OeeCounters {\n const a = this.acc.get(moverId)\n /*\n * **이 설비의 이벤트를 하나도 못 봤다** — 계측 창이 없다는 뜻이고, 그것을 `0` 으로 메우면\n * \"1970년부터 재고 있었다\" 가 된다. 시각이 밀리초 epoch 이므로 그 값이 곧 지금 시각이 되어,\n * 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다. 창을 **말하지 않는다** — 공식이\n * 없음을 없음으로 다룬다(§computeOee).\n */\n if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, scrapCount: 0 }\n const dt = Math.max(0, nowMs - a.lastMs)\n let busyMs = a.busyMs\n let downMs = a.downMs\n if (a.status === 'busy') busyMs += dt\n else if (a.status === 'down') downMs += dt\n return { runMs: busyMs, setupMs: 0, downMs, goodCount: a.goodCount, scrapCount: a.scrapCount, metricsSinceMs: a.startMs }\n }\n\n /** 공유 공식으로 근사 OEE 산출 — 정확한 OEE 는 실 텔레메트리(option ①). nowMs 기본=최신 이벤트 시각. */\n oee(moverId: string, nowMs: number = this.maxMs): OeeMetrics {\n return computeOee(this.counters(moverId, nowMs), nowMs)\n }\n}\n\n/**\n * live State 를 OEE 로 보강 — projector 미러 State(관측)의 무버에 누적기 OEE(계산)를 채운다.\n * live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 → equipment data(tag) payload 에 oee 포함\n * (sim 은 커널이 채움 — 이로써 live 도 동일 payload 형상). 순수(입력 불변, 얕은 복사).\n */\nexport function withLiveOee<S extends { equipment?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {\n if (!state?.equipment) return state\n return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }\n}\n"]}
1
+ {"version":3,"file":"oee-accumulator.js","sourceRoot":"","sources":["../../server/engine/oee-accumulator.ts"],"names":[],"mappings":";;;AA+LA,kCAGC;AAlMD;;;;;;;;;;;GAWG;AACH,+DAA+D;AAC/D,wDAAkE;AAoBlE,MAAa,cAAc;IAA3B;QACU,QAAG,GAAG,IAAI,GAAG,EAAe,CAAA;QAC5B,UAAK,GAAG,CAAC,CAAA,CAAC,kCAAkC;QAmB5C,uBAAkB,GAAG,IAAI,GAAG,EAAwC,CAAA;IAkI9E,CAAC;IA/HC;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,CAAC,KAA8G;QAC5H,IAAI,CAAC,YAAY,GAAG,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAA;QAChF,IAAI,CAAC,gBAAgB,GAAG,KAAK,EAAE,gBAAgB,CAAA;QAC/C,IAAI,CAAC,kBAAkB,CAAC,KAAK,EAAE,CAAA;QAC/B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,SAAS,IAAI,EAAE,EAAE,CAAC;YACvC,MAAM,GAAG,GAAG,CAAC,EAAE,YAAY,CAAA;YAC3B,IAAI,CAAC,EAAE,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM;gBAAE,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAA;QAC/F,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACK,WAAW,CAAC,OAAe;QACjC,OAAO,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,YAAY,CAAA;IAClE,CAAC;IAEO,MAAM,CAAC,OAAe,EAAE,CAAS;QACvC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC7B,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,CAAC,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAA;YACxF,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QAC1B,CAAC;QACD,OAAO,CAAC,CAAA;IACV,CAAC;IAED;;;;;;;OAOG;IACH,SAAS;QACP,OAAO;YACL,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;SACxE,CAAA;IACH,CAAC;IAED,gDAAgD;IAChD,OAAO,CAAC,EAAoC;QAC1C,IAAI,CAAC,KAAK,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,CAAA;QAC3B,IAAI,CAAC,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IACrF,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAAC,GAAsB;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACnC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK;YAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,SAAS,KAAK,gBAAgB,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,GAAG,CAAC,IAA6E,CAAA;YAC3F,IAAI,CAAC,CAAC,EAAE,OAAO;gBAAE,OAAM;YACvB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;YACnC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,CAAA,CAAC,8BAA8B;YACvE,CAAC,CAAC,kBAAkB,GAAG,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC,kBAAkB,CAAA;YACnE,OAAM;QACR,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,KAAK,kBAAkB;YAAE,OAAM;QAChD,MAAM,CAAC,GAAG,GAAG,CAAC,IAA6C,CAAA;QAC3D,IAAI,CAAC,CAAC,EAAE,OAAO;YAAE,OAAM;QACvB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;QACnC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,CAAC,CAAC,MAAM,IAAI,EAAE,CAAA;aAClC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,CAAC,CAAC,MAAM,IAAI,EAAE,CAAA;QAC5C,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAA;QACnB,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;IACd,CAAC;IAED,iFAAiF;IACjF,QAAQ,CAAC,OAAe,EAAE,QAAgB,IAAI,CAAC,KAAK;QAClD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC/B;;;;;WAKG;QACH,IAAI,CAAC,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,kBAAkB,EAAE,CAAC,EAAE,CAAA;QACvF,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;QACxC,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,CAAA;QACrB,IAAI,MAAM,GAAG,CAAC,CAAC,MAAM,CAAA;QACrB,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,MAAM,IAAI,EAAE,CAAA;aAChC,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;YAAE,MAAM,IAAI,EAAE,CAAA;QAC1C;;;;WAIG;QACH,MAAM,MAAM,GAAG,IAAA,6BAAc,EAAC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAA;QACjG,OAAO;YACL,KAAK,EAAE,MAAM;YACb,OAAO,EAAE,CAAC;YACV,MAAM;YACN,SAAS,EAAE,CAAC,CAAC,SAAS;YACtB,kBAAkB,EAAE,CAAC,CAAC,kBAAkB;YACxC,cAAc,EAAE,CAAC,CAAC,OAAO;YACzB,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5C,CAAA;IACH,CAAC;IAED,2EAA2E;IAC3E,GAAG,CAAC,OAAe,EAAE,QAAgB,IAAI,CAAC,KAAK;QAC7C,OAAO,IAAA,yBAAU,EAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,EAAE,KAAK,CAAC,CAAA;IACzD,CAAC;CACF;AAvJD,wCAuJC;AAED;;;;GAIG;AACH,SAAgB,WAAW,CAAkC,KAAQ,EAAE,GAAmB,EAAE,KAAc;IACxG,IAAI,CAAC,KAAK,EAAE,SAAS;QAAE,OAAO,KAAK,CAAA;IACnC,OAAO,EAAE,GAAG,KAAK,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,CAAA;AACjG,CAAC","sourcesContent":["/*\n * OEE 이벤트 누적기 (순수·근사) — live-모드 OEE 입력 생산의 한 경로(face2-inbound-live §1.1, option ②).\n *\n * 문제: OEE 공식(computeOee)은 sim/live 공유되나, 입력 카운터(runMs/downMs/…)는 미러 State·이벤트에 없다\n * (emitEquipment 는 status/location/motion 만 방출). → live 는 카운터를 별도 생산해야 한다.\n * 이 누적기: equipment.status 전이 스트림으로 무버별 시간-in-상태를 누적해 카운터를 근사 생산.\n *\n * 정확: downMs(고장 시간 = down 구간 합) · quality(good/scrap = quality.output 델타). 근사·한계:\n * - busyMs 를 runMs 로(setup 분리 불가 — 이벤트에 setup 없음) → performance 낙관·availability 는 setup 손실 미반영.\n * - quality.output 이 없으면(예: WMS/YMS) quality=1(품질 개념 없음).\n * 시간축 = Date.parse(eventTime)(ms) — 구간 차분은 sim/실시간 무관하게 정확(고정 오프셋 상쇄).\n */\n/* OEE 공식은 계약에 있다 — 소비자가 셋이라(시뮬·미러·MES) 커널에 두면 밖에서 다시 쓰게 된다. */\nimport { computeOee, nonWorkingMsIn } from '@operato/ops-contract'\nimport type { OeeCounters, WorkCalendarEntry } from '@operato/ops-contract'\nimport type { CanonicalEnvelope, OeeMetrics } from '@operato/ops-contract'\n\ninterface Acc {\n status?: string\n lastMs: number\n startMs: number\n busyMs: number\n downMs: number\n goodCount: number\n nonconformingCount: number\n}\n\n/** 누적기의 완전한 재개점 — 저장했다 되살릴 수 있어야 꼬리만 계산할 수 있다. */\nexport interface OeeCheckpoint {\n maxMs: number\n acc: ({ moverId: string } & Acc)[]\n}\n\nexport class OeeAccumulator {\n private acc = new Map<string, Acc>()\n private maxMs = 0 // 최신 이벤트 시각 — nowMs 기본값(현재 계측 시점)\n\n /*\n * ── 현장이 언제 도나 — **계획 조업 시간의 근거** (2026-09-07) ────────────────\n * 이것이 없던 동안 계획 조업 시간이 「재기 시작한 뒤 흐른 시간 전부」였다. 주말도 야간도 휴일도\n * 계획 조업으로 세었다.\n *\n * [산술] 8시간 × 주5일이면 168시간 중 40시간이 조업이다 — PBT 가 4.2배 크고 가동률이 그만큼 작다.\n * 그리고 그 수가 1로 잘려 있어서(오늘 걷었다) 그럴싸하게 보였다. 두 오류가 서로를 가렸다.\n *\n * plant 레인이 같은 자리를 먼저 겪고 실측을 냈다 — 3교대를 물려받으면 `availability 0.4583`,\n * 달력이 아예 없으면 **수를 안 낸다**. 트윈은 창 전체를 계획으로 쳐서 수를 냈다.\n *\n * ⚠ **선언이 없으면 채우지 않는다.** `nonWorkingMsIn` 이 그때 `undefined` 를 내고, 그 값을 0 으로\n * 바꾸지 않는다 — 0 은 「달력을 봤고 쉬는 시간이 없었다」는 주장이다. 지금은 그러면 PBT 가 창 전체가\n * 되어 예전과 같은 수가 나온다. **그것을 「안 낸다」로 바꿀지는 아직 결정되지 않았다** — 거동 변화가\n * 크고 화면이 비는 자리라 사용자 판단을 기다린다(성숙도에 미달로 세우는 짝과 함께 가야 한다).\n */\n private siteCalendar?: readonly WorkCalendarEntry[]\n private calendarByResource = new Map<string, readonly WorkCalendarEntry[]>()\n private utcOffsetMinutes?: number\n\n /**\n * 모델에서 조업 달력을 읽어 얹는다 — 모델을 들고 있는 쪽(`TwinEngine`)이 부른다.\n *\n * ── 우선순위가 여기 있는 이유 ───────────────────────────────────────────────\n * ```\n * 설비의 달력 이 설비의 가동 계획 — 선언했으면 이것이 이긴다 (24시간 도는 로)\n * 현장의 달력 이 현장의 조업일\n * 없음 제약 없음 — 계획 조업 시간이 창 전체가 된다\n * ```\n * 계획 조업 시간을 내는 자리가 여기라 그 규칙도 여기 둔다. 부르는 쪽마다 고르게 하면 라이브와\n * 히스토리가 다른 답을 낸다.\n *\n * ⚠ **설비의 달력은 사람의 근무조가 아니다.** 씨앗이 사람의 근무조를 설비 달력에 복사한 일이 있었고,\n * 그때 아무도 근무하지 않는 시간이 근무 시간으로 기록됐다. 사람 쪽은 `persons[].workCalendar` 다.\n *\n * 세우는 자리에서 한 번만 주지 않고 따로 부르는 이유는 모델이 다시 실릴 수 있기 때문이다\n * (재동기화 · 이관). 그때 달력만 갈아 끼우면 누적된 계측을 버리지 않는다.\n */\n setWorkCalendar(model?: { workCalendar?: readonly WorkCalendarEntry[]; equipment?: readonly any[]; utcOffsetMinutes?: number }): void {\n this.siteCalendar = model?.workCalendar?.length ? model.workCalendar : undefined\n this.utcOffsetMinutes = model?.utcOffsetMinutes\n this.calendarByResource.clear()\n for (const e of model?.equipment ?? []) {\n const cal = e?.workCalendar\n if (e?.id && Array.isArray(cal) && cal.length) this.calendarByResource.set(String(e.id), cal)\n }\n }\n\n /**\n * 이 설비의 계획 가동 달력 — 자기 것이 있으면 그것, 없으면 현장의 것.\n *\n * 인자 이름이 `moverId` 인 것은 **봉투의 옛 세대 필드 이름**이라 그렇다(§`apply`). 새로 만드는\n * 이름에는 그 낱말을 쓰지 않는다 — 어휘 가드가 그것을 잡았고, 맞게 잡았다.\n */\n private calendarFor(moverId: string): readonly WorkCalendarEntry[] | undefined {\n return this.calendarByResource.get(moverId) ?? this.siteCalendar\n }\n\n private ensure(moverId: string, t: number): Acc {\n let a = this.acc.get(moverId)\n if (!a) {\n a = { lastMs: t, startMs: t, busyMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0 }\n this.acc.set(moverId, a)\n }\n return a\n }\n\n /**\n * **재개점** — 이 누적기의 상태 전부(이어 계산의 씨앗).\n *\n * ── 왜 필요한가 (2026-08-18) ────────────────────────────────────────────────\n * 가동 이력은 원 시스템이 누적을 주지 않아 **우리가 이벤트를 적분해** 만든다. 그래서 저널을 꼬리만\n * 계산하면 이 누적기는 꼬리분만 갖게 되고, 화면의 가용률·수율이 알리지 않고 작아진다(오류는 나지 않는다).\n * 재개점을 함께 저장·복원해야 「이어 계산한 값」이 「0부터 계산한 값」과 같아진다.\n */\n serialize(): OeeCheckpoint {\n return {\n maxMs: this.maxMs,\n acc: [...this.acc.entries()].map(([moverId, a]) => ({ moverId, ...a }))\n }\n }\n\n /** 재개점에서 되세운다 — 모르는 것은 지어내지 않는다(없으면 빈 누적기다). */\n restore(cp: OeeCheckpoint | undefined | null): void {\n this.maxMs = cp?.maxMs ?? 0\n this.acc = new Map((cp?.acc ?? []).map(({ moverId, ...a }) => [moverId, { ...a }]))\n }\n\n /** equipment.status(시간-in-상태) + quality.output(양품/불량) 소비. 다른 eventType 무시. */\n apply(env: CanonicalEnvelope): void {\n const t = Date.parse(env.eventTime)\n if (t > this.maxMs) this.maxMs = t\n if (env.eventType === 'quality.output') {\n const q = env.data as { moverId?: string; goodCount?: number; nonconformingCount?: number }\n if (!q?.moverId) return\n const a = this.ensure(q.moverId, t)\n a.goodCount = q.goodCount ?? a.goodCount // 누적값 최신화(good/scrap 은 무버 누적)\n a.nonconformingCount = q.nonconformingCount ?? a.nonconformingCount\n return\n }\n if (env.eventType !== 'equipment.status') return\n const d = env.data as { moverId?: string; status?: string }\n if (!d?.moverId) return\n const a = this.ensure(d.moverId, t)\n const dt = Math.max(0, t - a.lastMs)\n if (a.status === 'busy') a.busyMs += dt\n else if (a.status === 'down') a.downMs += dt\n a.status = d.status\n a.lastMs = t\n }\n\n /** 무버 OEE 카운터 — 마지막 상태 구간을 nowMs(기본=최신 이벤트 시각)까지 flush. busy→run 근사, setup=0. */\n counters(moverId: string, nowMs: number = this.maxMs): OeeCounters {\n const a = this.acc.get(moverId)\n /*\n * **이 설비의 이벤트를 하나도 못 봤다** — 계측 창이 없다는 뜻이고, 그것을 `0` 으로 메우면\n * \"1970년부터 재고 있었다\" 가 된다. 시각이 밀리초 epoch 이므로 그 값이 곧 지금 시각이 되어,\n * 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다. 창을 **말하지 않는다** — 공식이\n * 없음을 없음으로 다룬다(§computeOee).\n */\n if (!a) return { runMs: 0, setupMs: 0, downMs: 0, goodCount: 0, nonconformingCount: 0 }\n const dt = Math.max(0, nowMs - a.lastMs)\n let busyMs = a.busyMs\n let downMs = a.downMs\n if (a.status === 'busy') busyMs += dt\n else if (a.status === 'down') downMs += dt\n /*\n * 계획 정지 — 계약이 이 칸에 그 뜻을 적어 뒀다(점심 · 예방보전 · 교대). 휴일과 주말이 같은\n * 갈래다. `metricsSinceMs` 를 당겨서 같은 효과를 내지 않는다: 그 칸은 「언제부터 쟀나」라는\n * 사실이고, 파생값을 얹으면 실제보다 늦게 재기 시작했다고 말하는 것이 된다.\n */\n const holdMs = nonWorkingMsIn(this.calendarFor(moverId), a.startMs, nowMs, this.utcOffsetMinutes)\n return {\n runMs: busyMs,\n setupMs: 0,\n downMs,\n goodCount: a.goodCount,\n nonconformingCount: a.nonconformingCount,\n metricsSinceMs: a.startMs,\n ...(holdMs !== undefined ? { holdMs } : {})\n }\n }\n\n /** 공유 공식으로 근사 OEE 산출 — 정확한 OEE 는 실 텔레메트리(option ①). nowMs 기본=최신 이벤트 시각. */\n oee(moverId: string, nowMs: number = this.maxMs): OeeMetrics {\n return computeOee(this.counters(moverId, nowMs), nowMs)\n }\n}\n\n/**\n * live State 를 OEE 로 보강 — projector 미러 State(관측)의 무버에 누적기 OEE(계산)를 채운다.\n * live-모드 publishEntityData 가 buildEntityDeltas 전에 적용 → equipment data(tag) payload 에 oee 포함\n * (sim 은 커널이 채움 — 이로써 live 도 동일 payload 형상). 순수(입력 불변, 얕은 복사).\n */\nexport function withLiveOee<S extends { equipment?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {\n if (!state?.equipment) return state\n return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }\n}\n"]}
@@ -597,8 +597,23 @@ export declare class TwinEngine {
597
597
  * `timezone` 을 읽는다. 공간이 말하지 않으면 **테넌트로 내려가지 않는다** — 잘못된 입자로 답하는 것이
598
598
  * 모르는 것보다 나쁘다(그때는 커널이 UTC 로 읽고, 그 기본값은 계약에 밝혀져 있다).
599
599
  *
600
- * 커널은 zero-dep 이라 시간대 데이터베이스를 갖지 않으므로 **분 오프셋**으로 풀어 넘긴다. 그 값은
601
- * 지금 계절의 것이다(일광절약시간) — 계절을 넘는 긴 예측은 한 시간 어긋난다(계약에 명시).
600
+ * 커널은 zero-dep 이라 시간대 데이터베이스를 갖지 않으므로 **분 오프셋**으로 풀어 넘긴다.
601
+ *
602
+ * ── 이름도 함께 얹는다 (2026-09-07) ────────────────────────────────────────
603
+ * 오프셋 하나만 얹으면 **그 값이 이 순간의 것으로 모델에 굳는다.** 트윈은 몇 주씩 돌고, 자료에
604
+ * 이미 `America/Tijuana`(PST −480 / PDT −420)가 있다 — 여름에 뜬 트윈이 11월까지 돌면 그 뒤 모든
605
+ * 시각이 한 시간 틀린다.
606
+ *
607
+ * 지난 구간을 접을 때가 더 나쁘다. 1년치를 교대별로 접으면 그 1년을 한 오프셋으로 읽는데 그 안에
608
+ * 전환이 두 번 있다. 어긋난 시각이 오류를 내지 않고 **다른 교대로 세어진다.**
609
+ *
610
+ * 그래서 이름(`timezone`)을 함께 얹어, 순간마다 다시 풀 수 있게 한다. 커널은 이 값을 쓰지 않는다 —
611
+ * 여전히 오프셋을 호출마다 받는다. 푸는 것은 호스트의 몫이고 그것이 계약이 적어 둔 분담이다.
612
+ *
613
+ * `utcOffsetMinutes` 도 계속 얹는다. 아직 이름을 안 보는 소비처가 그 값으로 돌고, 이름이 없는 공간에
614
+ * 대해서는 그것이 유일한 답이다.
615
+ *
616
+ * ⚠ 이것으로도 완전하지 않다. 정확한 답은 호스트가 표준대로 **절대 구간**을 계산해 넣는 것이다.
602
617
  */
603
618
  static withSpaceTimeBase(model: TwinModelDef, domainId: string, spaceId?: string): Promise<TwinModelDef>;
604
619
  static startLive(id: string, domainId: string, kind: string, model: TwinModelDef): InstanceRuntime;
@@ -1181,6 +1196,28 @@ export declare class TwinEngine {
1181
1196
  */
1182
1197
  static modeOf(domainId: string, id: string): 'live' | 'sim' | undefined;
1183
1198
  static owns(domainId: string, id: string): boolean;
1199
+ /**
1200
+ * 이 트윈의 **중복 판정 기억을 미리 채운다** — 저널에서 읽은 정체로.
1201
+ *
1202
+ * 기억은 프로세스 안에만 있어서 재기동하면 비워진다(§`FactDeduper`). 그 뒤 backfill 을 돌리면
1203
+ * 이미 저널에 있는 사실도 「처음 본 것」이 되어 다시 앉는다. 실측으로 39건이 통째로 다시 앉았다
1204
+ * (2026-09-06, `mes-line-a`).
1205
+ *
1206
+ * 안 도는 트윈에는 채울 곳이 없다 — 0을 낸다(거짓이 아니라 사실이다).
1207
+ */
1208
+ static warmDeduper(domainId: string, id: string, identities: readonly string[]): number;
1209
+ /**
1210
+ * 이 프로세스가 지금 들고 있는 트윈들 — 테넌트로 좁혀서.
1211
+ *
1212
+ * 주기 작업(규칙 판정 등)이 「무엇을 돌아야 하나」를 물을 자리다. 등록부(DB)를 읽으면 **다른 프로세스가
1213
+ * 든 트윈까지 나온다** — 그것을 돌면 같은 조치가 두 벌 올라간다. 도는 것은 든 쪽이 돈다.
1214
+ *
1215
+ * `owns()` 와 같은 술어를 쓴다(`this.instances`). 두 벌이면 어긋난다.
1216
+ */
1217
+ static ownedTwins(domainId?: string): {
1218
+ domainId: string;
1219
+ instanceId: string;
1220
+ }[];
1184
1221
  /**
1185
1222
  * 이 트윈이 이 테넌트 것인가 — **떠 있든 아니든.**
1186
1223
  *
@@ -1250,6 +1287,30 @@ export declare class TwinEngine {
1250
1287
  domainId: string;
1251
1288
  instanceId: string;
1252
1289
  }, closed: IngestWindow) => void): void;
1290
+ /**
1291
+ * **모든 트윈의 창을 굴린다** — 보는 사람이 없어도 창이 닫히게.
1292
+ *
1293
+ * ── 무엇이 있었나 (2026-09-05 실측) ────────────────────────────────────────
1294
+ * 창은 「다음 사건이 왔을 때 이전 창을 닫는」 모양으로만 굴렀다. 그러면 사실이 드문 트윈은 창이
1295
+ * 계속 열려 있고, 표에 아무것도 안 남는다. 두 수가 그것을 정확히 말한다.
1296
+ *
1297
+ * mes-miratek 사실 46건 · 62분에 흩어짐 창 1개
1298
+ * mes-line-a 사실 11건 창 48개
1299
+ *
1300
+ * **사실 수와 창 수가 거꾸로다.** 창을 만든 것이 유입이 아니라 **누가 물어본 것**이다 —
1301
+ * `ingestHealthOf()` 가 굴리므로, 점검표·화면이 계속 묻는 트윈만 기록이 쌓였다.
1302
+ *
1303
+ * 그래서 **아무도 안 보는 트윈일수록 기록이 없다.** 사실이 조용히 사라지는 것을 잡으려고 만든
1304
+ * 장부가 정확히 그 반대로 돌고 있었다. 실제로 137봉투가 장부 없이 지나갔다(인티그레이션 레인 실측).
1305
+ *
1306
+ * ── 왜 주기인가 ────────────────────────────────────────────────────────────
1307
+ * 창이 닫히는 이유는 **시간이 지난 것**이다. 사건도 질의도 그것을 알려 주는 신호일 뿐이고, 둘 다
1308
+ * 안 올 수 있다. 시간은 늘 지나므로 주기가 그 사실에 맞는 신호다.
1309
+ *
1310
+ * 조용한 트윈이 자기 침묵을 기록하게 되는 것이 이 변경의 값이다 — `expected-quiet` 판정이 그
1311
+ * 기록 위에 선다.
1312
+ */
1313
+ static rollAllIngestWindows(nowMs?: number): number;
1253
1314
  /**
1254
1315
  * 한 번의 인제스트 결과를 장부에 적는다.
1255
1316
  *
@@ -1296,7 +1357,15 @@ export declare class TwinEngine {
1296
1357
  *
1297
1358
  * 「통과」로 세지 않는다 — 트윈이 멈춘 사이 사실이 사라지는데 화면이 100% 라고 말하게 된다.
1298
1359
  */
1299
- undelivered?: number): void;
1360
+ undelivered?: number,
1361
+ /**
1362
+ * **저널에 안 간 어휘 요소의 수** — 로트 번호·소비기한처럼 시각축이 없는 값.
1363
+ *
1364
+ * 사라진 것이 아니라 저널로 가지 않는 것이다. 그런데 그 수를 빼 주는 자리가 없으면
1365
+ * 「보낸 것 − 앉은 것」이 안 맞고 **그 차이를 사라진 것으로 읽게 된다**(2026-09-05 실측:
1366
+ * 커서 20 · 저널 17 · 어휘 1 을 사람이 손으로 세서 「둘이 빈다」를 찾았다).
1367
+ */
1368
+ vocabulary?: number): void;
1300
1369
  /**
1301
1370
  * **원본에 닿지 못했다**를 적는다 — 「받은 것이 없다」와 가른다(§`recordReadFailure`).
1302
1371
  *
@@ -1539,8 +1539,23 @@ class TwinEngine {
1539
1539
  * `timezone` 을 읽는다. 공간이 말하지 않으면 **테넌트로 내려가지 않는다** — 잘못된 입자로 답하는 것이
1540
1540
  * 모르는 것보다 나쁘다(그때는 커널이 UTC 로 읽고, 그 기본값은 계약에 밝혀져 있다).
1541
1541
  *
1542
- * 커널은 zero-dep 이라 시간대 데이터베이스를 갖지 않으므로 **분 오프셋**으로 풀어 넘긴다. 그 값은
1543
- * 지금 계절의 것이다(일광절약시간) — 계절을 넘는 긴 예측은 한 시간 어긋난다(계약에 명시).
1542
+ * 커널은 zero-dep 이라 시간대 데이터베이스를 갖지 않으므로 **분 오프셋**으로 풀어 넘긴다.
1543
+ *
1544
+ * ── 이름도 함께 얹는다 (2026-09-07) ────────────────────────────────────────
1545
+ * 오프셋 하나만 얹으면 **그 값이 이 순간의 것으로 모델에 굳는다.** 트윈은 몇 주씩 돌고, 자료에
1546
+ * 이미 `America/Tijuana`(PST −480 / PDT −420)가 있다 — 여름에 뜬 트윈이 11월까지 돌면 그 뒤 모든
1547
+ * 시각이 한 시간 틀린다.
1548
+ *
1549
+ * 지난 구간을 접을 때가 더 나쁘다. 1년치를 교대별로 접으면 그 1년을 한 오프셋으로 읽는데 그 안에
1550
+ * 전환이 두 번 있다. 어긋난 시각이 오류를 내지 않고 **다른 교대로 세어진다.**
1551
+ *
1552
+ * 그래서 이름(`timezone`)을 함께 얹어, 순간마다 다시 풀 수 있게 한다. 커널은 이 값을 쓰지 않는다 —
1553
+ * 여전히 오프셋을 호출마다 받는다. 푸는 것은 호스트의 몫이고 그것이 계약이 적어 둔 분담이다.
1554
+ *
1555
+ * `utcOffsetMinutes` 도 계속 얹는다. 아직 이름을 안 보는 소비처가 그 값으로 돌고, 이름이 없는 공간에
1556
+ * 대해서는 그것이 유일한 답이다.
1557
+ *
1558
+ * ⚠ 이것으로도 완전하지 않다. 정확한 답은 호스트가 표준대로 **절대 구간**을 계산해 넣는 것이다.
1544
1559
  */
1545
1560
  static async withSpaceTimeBase(model, domainId, spaceId) {
1546
1561
  const sid = spaceId ?? model?.spaceId;
@@ -1553,7 +1568,8 @@ class TwinEngine {
1553
1568
  (0, log_js_1.twinWarn)(`[twin-engine] space "${sid}" declares time zone "${space.timezone}" but it is not a known IANA zone — times will be read as UTC.`);
1554
1569
  return model;
1555
1570
  }
1556
- return { ...model, utcOffsetMinutes: offset };
1571
+ /* 이름이 IANA 읽히는 것을 위에서 이미 확인했다 — 오프셋이 나왔다는 것이 그 증거다. */
1572
+ return { ...model, utcOffsetMinutes: offset, timezone: space.timezone };
1557
1573
  }
1558
1574
  static startLive(id, domainId, kind, model) {
1559
1575
  const key = (0, runtime_key_js_1.runtimeKey)(domainId, id);
@@ -1570,7 +1586,13 @@ class TwinEngine {
1570
1586
  /* `projector` 필드는 옛 이름으로 남긴다 — 소비처가 `snapshot()` 을 부르므로 얇은 어댑터로 잇는다.
1571
1587
  * (P3 에서 소비처를 커널 어휘로 바꾸면 사라진다.) */
1572
1588
  const projector = { apply: (e) => kernel.apply(e), snapshot: () => kernel.getSnapshot() };
1573
- const inst = { id, domainId, mode: 'live', restartPolicy: 'resync', kernel, projector, oee: new oee_accumulator_js_1.OeeAccumulator(), spaceId: model?.spaceId, model, unsub: () => { } };
1589
+ const oeeAcc = new oee_accumulator_js_1.OeeAccumulator();
1590
+ /*
1591
+ * **현장의 조업 달력을 얹는다** — 이것을 안 부르면 계획 조업 시간이 창 전체가 된다(§`OeeAccumulator`).
1592
+ * 자리를 만들고 부르지 않는 것이 이 저장소에서 가장 자주 나는 결함이라, 세우는 자리에서 바로 붙인다.
1593
+ */
1594
+ oeeAcc.setWorkCalendar(model);
1595
+ const inst = { id, domainId, mode: 'live', restartPolicy: 'resync', kernel, projector, oee: oeeAcc, spaceId: model?.spaceId, model, unsub: () => { } };
1574
1596
  /*
1575
1597
  * ── 커널이 **판정으로 낸 사실**도 저널에 남는다 (2026-08-14 실측으로 잡음) ────
1576
1598
  *
@@ -3581,6 +3603,12 @@ class TwinEngine {
3581
3603
  * 가동 이력이 나온다(그 뒤에 일어난 고장이 과거 화면에 섞이지 않는다).
3582
3604
  */
3583
3605
  const oee = new oee_accumulator_js_1.OeeAccumulator();
3606
+ /*
3607
+ * 히스토리도 같은 달력으로 읽는다 — 라이브와 다른 기준으로 세면 두 화면이 다른 수를 보인다.
3608
+ * 여기서 쓸 모델은 `newest` 다(이 스코프의 마지막 구조). 구간마다의 `model` 은 위 루프 안에만
3609
+ * 있고, 달력은 구조 개정마다 갈리는 값이 아니다.
3610
+ */
3611
+ oee.setWorkCalendar(newest);
3584
3612
  /* 재개점이 있으면 그 위에 꼬리만 얹는다 — 없으면 꼬리분만 세어 가용률이 알리지 않고 작아진다. */
3585
3613
  if (resume?.fold?.oee)
3586
3614
  oee.restore(resume.fold.oee);
@@ -3941,6 +3969,34 @@ class TwinEngine {
3941
3969
  static owns(domainId, id) {
3942
3970
  return !!this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
3943
3971
  }
3972
+ /**
3973
+ * 이 트윈의 **중복 판정 기억을 미리 채운다** — 저널에서 읽은 정체로.
3974
+ *
3975
+ * 기억은 프로세스 안에만 있어서 재기동하면 비워진다(§`FactDeduper`). 그 뒤 backfill 을 돌리면
3976
+ * 이미 저널에 있는 사실도 「처음 본 것」이 되어 다시 앉는다. 실측으로 39건이 통째로 다시 앉았다
3977
+ * (2026-09-06, `mes-line-a`).
3978
+ *
3979
+ * 안 도는 트윈에는 채울 곳이 없다 — 0을 낸다(거짓이 아니라 사실이다).
3980
+ */
3981
+ static warmDeduper(domainId, id, identities) {
3982
+ const inst = this.instances[(0, runtime_key_js_1.runtimeKey)(domainId, id)];
3983
+ if (!inst || !identities.length)
3984
+ return 0;
3985
+ return (inst.deduper ??= new ingest_dedupe_js_1.FactDeduper()).remember(identities);
3986
+ }
3987
+ /**
3988
+ * 이 프로세스가 지금 들고 있는 트윈들 — 테넌트로 좁혀서.
3989
+ *
3990
+ * 주기 작업(규칙 판정 등)이 「무엇을 돌아야 하나」를 물을 자리다. 등록부(DB)를 읽으면 **다른 프로세스가
3991
+ * 든 트윈까지 나온다** — 그것을 돌면 같은 조치가 두 벌 올라간다. 도는 것은 든 쪽이 돈다.
3992
+ *
3993
+ * `owns()` 와 같은 술어를 쓴다(`this.instances`). 두 벌이면 어긋난다.
3994
+ */
3995
+ static ownedTwins(domainId) {
3996
+ return Object.entries(this.instances)
3997
+ .map(([key, inst]) => ({ domainId: inst.domainId, instanceId: (0, runtime_key_js_1.parseRuntimeKey)(key).instanceId }))
3998
+ .filter(t => !domainId || t.domainId === domainId);
3999
+ }
3944
4000
  /**
3945
4001
  * 이 트윈이 이 테넌트 것인가 — **떠 있든 아니든.**
3946
4002
  *
@@ -4098,6 +4154,53 @@ class TwinEngine {
4098
4154
  static onIngestWindowClosed(fn) {
4099
4155
  this.onWindowClosed = fn;
4100
4156
  }
4157
+ /**
4158
+ * **모든 트윈의 창을 굴린다** — 보는 사람이 없어도 창이 닫히게.
4159
+ *
4160
+ * ── 무엇이 있었나 (2026-09-05 실측) ────────────────────────────────────────
4161
+ * 창은 「다음 사건이 왔을 때 이전 창을 닫는」 모양으로만 굴렀다. 그러면 사실이 드문 트윈은 창이
4162
+ * 계속 열려 있고, 표에 아무것도 안 남는다. 두 수가 그것을 정확히 말한다.
4163
+ *
4164
+ * mes-miratek 사실 46건 · 62분에 흩어짐 창 1개
4165
+ * mes-line-a 사실 11건 창 48개
4166
+ *
4167
+ * **사실 수와 창 수가 거꾸로다.** 창을 만든 것이 유입이 아니라 **누가 물어본 것**이다 —
4168
+ * `ingestHealthOf()` 가 굴리므로, 점검표·화면이 계속 묻는 트윈만 기록이 쌓였다.
4169
+ *
4170
+ * 그래서 **아무도 안 보는 트윈일수록 기록이 없다.** 사실이 조용히 사라지는 것을 잡으려고 만든
4171
+ * 장부가 정확히 그 반대로 돌고 있었다. 실제로 137봉투가 장부 없이 지나갔다(인티그레이션 레인 실측).
4172
+ *
4173
+ * ── 왜 주기인가 ────────────────────────────────────────────────────────────
4174
+ * 창이 닫히는 이유는 **시간이 지난 것**이다. 사건도 질의도 그것을 알려 주는 신호일 뿐이고, 둘 다
4175
+ * 안 올 수 있다. 시간은 늘 지나므로 주기가 그 사실에 맞는 신호다.
4176
+ *
4177
+ * 조용한 트윈이 자기 침묵을 기록하게 되는 것이 이 변경의 값이다 — `expected-quiet` 판정이 그
4178
+ * 기록 위에 선다.
4179
+ */
4180
+ static rollAllIngestWindows(nowMs = Date.now()) {
4181
+ let closed = 0;
4182
+ for (const key of Object.keys(this.ingestLedgers)) {
4183
+ const ledger = this.ingestLedgers[key];
4184
+ if (!ledger)
4185
+ continue;
4186
+ /*
4187
+ * 키가 깨져 있으면 그 트윈만 건너뛴다 — 하나 때문에 나머지 장부가 안 굴러서는 안 된다.
4188
+ * 훅의 실패도 `rollIngestWindow` 가 감싼다(영속을 지키려고 관측을 멈추지 않는다).
4189
+ */
4190
+ let at;
4191
+ try {
4192
+ at = (0, runtime_key_js_1.parseRuntimeKey)(key);
4193
+ }
4194
+ catch {
4195
+ continue;
4196
+ }
4197
+ (0, ingest_health_js_1.rollIngestWindow)(ledger, nowMs, undefined, w => {
4198
+ closed++;
4199
+ this.onWindowClosed?.(at, w);
4200
+ });
4201
+ }
4202
+ return closed;
4203
+ }
4101
4204
  /**
4102
4205
  * 한 번의 인제스트 결과를 장부에 적는다.
4103
4206
  *
@@ -4155,10 +4258,20 @@ class TwinEngine {
4155
4258
  *
4156
4259
  * 「통과」로 세지 않는다 — 트윈이 멈춘 사이 사실이 사라지는데 화면이 100% 라고 말하게 된다.
4157
4260
  */
4158
- undelivered = 0) {
4261
+ undelivered = 0,
4262
+ /**
4263
+ * **저널에 안 간 어휘 요소의 수** — 로트 번호·소비기한처럼 시각축이 없는 값.
4264
+ *
4265
+ * 사라진 것이 아니라 저널로 가지 않는 것이다. 그런데 그 수를 빼 주는 자리가 없으면
4266
+ * 「보낸 것 − 앉은 것」이 안 맞고 **그 차이를 사라진 것으로 읽게 된다**(2026-09-05 실측:
4267
+ * 커서 20 · 저널 17 · 어휘 1 을 사람이 손으로 세서 「둘이 빈다」를 찾았다).
4268
+ */
4269
+ vocabulary = 0) {
4159
4270
  const key = (0, runtime_key_js_1.runtimeKey)(domainId, instanceId);
4160
4271
  const ledger = this.ingestLedgers[key] ?? (this.ingestLedgers[key] = (0, ingest_health_js_1.newIngestLedger)());
4161
4272
  (0, ingest_health_js_1.recordIngest)(ledger, offered, rejected, nowMs, closed => this.onWindowClosed?.({ domainId, instanceId }, closed), undelivered);
4273
+ /* 창을 이미 굴린 뒤라 순서가 중요하다 — 먼저 적으면 한 유입이 두 창에 걸친다. */
4274
+ (0, ingest_health_js_1.recordVocabulary)(ledger, vocabulary);
4162
4275
  }
4163
4276
  /**
4164
4277
  * **원본에 닿지 못했다**를 적는다 — 「받은 것이 없다」와 가른다(§`recordReadFailure`).