@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
@@ -1 +1 @@
1
- {"version":3,"file":"twin-space.js","sourceRoot":"","sources":["../../../server/service/twin-space/twin-space.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\n/*\n * TwinSpace — 테넌트 1급 공간(Site) 정의(ADR-0016). 트윈보다 먼저 세우는 몰입의 무대.\n * 공간이 보이는 방식(레이아웃·배경·영역)은 정규화됨: TwinSpaceRepresentation(map|image|model —\n * 이미지 배경은 attachment-base 참조 `config.imageUrl`) + TwinSpaceArea. 인스턴스는 model.spaceId 로 바인딩.\n * - geo = 사이트 지오레퍼런스, primaryRepresentationId = 홈/모니터 대표 표현.\n * - content(simple-json) 는 정규화 이전 레거시 단일 blob(@deprecated, P6 제거) — 신규 저작은 representation/area 로.\n * (CLAUDE.md: 모든 @ObjectType/@Field 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_space_0', (e: TwinSpace) => [e.domain, e.spaceId], { unique: true })\n@ObjectType({ description: 'A tenant-level twin Space (Site) — the shared spatial stage set up before instances; its layout, backdrop and areas are normalized into representations (image backdrops via attachment-base) and areas.' })\nexport class TwinSpace {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the space record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinSpace) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Space id (unique within a domain).' })\n spaceId: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Human-readable space name.' })\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Space description.' })\n description?: string\n\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'Site georeference { lat, lon, rotation } — pins the design frame to the earth so every representation lands in the same place. `rotation` is degrees clockwise from true north (bearing convention: 0 = north, 90 = east) by which the design frame is turned.' })\n geo?: any\n\n /*\n * 설계 좌표계의 범위 — **미터**. `geo` 와 한 쌍이다.\n *\n * 구역(TwinArea.layout)은 표현 중립 설계 좌표 `{x, y, w, h}` 로 놓인다. 그 좌표가 어디까지인지를\n * 이 둘이 정하고, `geo` 가 그것을 지구에 붙인다 — 지도로 보든 도면 이미지로 보든 보드로 보든\n * 구역이 같은 자리에 있어야 하므로, 범위도 지오레퍼런스도 **표현이 아니라 공간**이 갖는다.\n *\n * 설계 좌표 (x, y) ──[extentX/extentY 로 중심 이동]──▶ 미터 오프셋 ──[geo]──▶ 위경도\n *\n * `width`/`depth` 라 부르지 않는 이유: 컴포넌트 속성의 `depth` 는 높이 축(z)이다. 같은 화면에서\n * 한 낱말이 두 축을 가리키면 읽는 사람이 매번 어느 쪽인지 물어야 한다.\n * 단위 필드는 두지 않는다 — 미터로 고정이고, 있으면 \"단위를 지원한다\" 는 거짓 신호가 된다.\n */\n @Column({ type: 'float', nullable: true })\n @Field({ nullable: true, description: 'Extent of the design frame along the east-west axis, in metres. Area layouts are placed within this frame.' })\n extentX?: number\n\n @Column({ type: 'float', nullable: true })\n @Field({ nullable: true, description: 'Extent of the design frame along the north-south axis, in metres. Area layouts are placed within this frame.' })\n extentY?: number\n\n /**\n * 이 현장의 시각 기준 — IANA 이름(`'America/Tijuana'`).\n *\n * **테넌트가 아니라 공간이 갖는다.** 한 테넌트가 Rosarito(태평양)와 한국 공장을 함께 가질 수 있고,\n * 테넌트 단위(`Domain.timezone`)로 두면 둘 중 하나는 반드시 틀린다. 교대가 \"06시\" 라고 할 때 그것이\n * 어느 06시인가는 **물리 현장의 성질**이다.\n *\n * 오프셋 숫자가 아니라 이름으로 둔다: 숫자로는 일광절약시간을 표현할 수 없고, 나중에 절대 구간\n * (표준 `StartDateTime`)으로 옮길 때도 이름이 있어야 계산할 수 있다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'IANA time zone of this physical site (e.g. America/Tijuana) — the base for reading shift times. Belongs to the space, not the tenant, because one tenant can run plants in different zones.' })\n timezone?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the primary representation shown on the home/monitor.' })\n primaryRepresentationId?: string\n\n /**\n * 이 **현장**의 로컬 선언 겹 — 요금처럼 **트윈이 아니라 현장이 정하는 수**.\n *\n * ── 왜 트윈에 두면 안 되나 (2026-08-18) ────────────────────────────────────\n * 요금 단가·통화는 그 현장의 계약이므로, 같은 수전을 보는 EMS 트윈이 둘이면 **같은 값을 두 번**\n * 선언해야 했다. 두 번 적는 값은 반드시 갈라진다(하나만 고치는 날이 온다) — 그때 두 트윈이 다른\n * 금액을 말하고, 어느 쪽이 맞는지 화면으로는 알 수 없다.\n *\n * 적용 순서는 **현장 먼저, 트윈이 나중**이다. 더 좁은 선언이 이긴다 — 한 현장 안에서 특정 트윈만\n * 다른 값으로 실험하는 일이 있고(예: 인상된 단가로 what-if), 그때 현장 값을 고치게 만들면 나머지\n * 트윈이 함께 흔들린다.\n *\n * 모양은 트윈 겹과 같다(`LocalDeclaration`) — 같은 규칙(검사·기억·철회)을 두 벌로 만들지 않는다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: \"Locally-declared values that belong to this facility rather than to one twin (tariff, currency). Applied to every twin in the space before the twin's own declarations, which win when both name the same property.\" })\n localDeclarations?: any\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the space was created.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the space was last updated.' })\n updatedAt?: Date\n}\n"]}
1
+ {"version":3,"file":"twin-space.js","sourceRoot":"","sources":["../../../server/service/twin-space/twin-space.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\n/*\n * TwinSpace — 테넌트 1급 공간(Site) 정의(ADR-0016). 트윈보다 먼저 세우는 몰입의 무대.\n * 공간이 보이는 방식(레이아웃·배경·영역)은 정규화됨: TwinSpaceRepresentation(map|image|model —\n * 이미지 배경은 attachment-base 참조 `config.imageUrl`) + TwinSpaceArea. 인스턴스는 model.spaceId 로 바인딩.\n * - geo = 사이트 지오레퍼런스, primaryRepresentationId = 홈/모니터 대표 표현.\n * - content(simple-json) 는 정규화 이전 레거시 단일 blob(@deprecated, P6 제거) — 신규 저작은 representation/area 로.\n * (CLAUDE.md: 모든 @ObjectType/@Field 영문 description 필수.)\n */\n@Entity()\n@Index('ix_twin_space_0', (e: TwinSpace) => [e.domain, e.spaceId], { unique: true })\n@ObjectType({ description: 'A tenant-level twin Space (Site) — the shared spatial stage set up before instances; its layout, backdrop and areas are normalized into representations (image backdrops via attachment-base) and areas.' })\nexport class TwinSpace {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the space record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinSpace) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Space id (unique within a domain).' })\n spaceId: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Human-readable space name.' })\n name?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Space description.' })\n description?: string\n\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'Site georeference { lat, lon, rotation } — pins the design frame to the earth so every representation lands in the same place. `rotation` is degrees clockwise from true north (bearing convention: 0 = north, 90 = east) by which the design frame is turned.' })\n geo?: any\n\n /*\n * 설계 좌표계의 범위 — **미터**. `geo` 와 한 쌍이다.\n *\n * 구역(TwinArea.layout)은 표현 중립 설계 좌표 `{x, y, w, h}` 로 놓인다. 그 좌표가 어디까지인지를\n * 이 둘이 정하고, `geo` 가 그것을 지구에 붙인다 — 지도로 보든 도면 이미지로 보든 보드로 보든\n * 구역이 같은 자리에 있어야 하므로, 범위도 지오레퍼런스도 **표현이 아니라 공간**이 갖는다.\n *\n * 설계 좌표 (x, y) ──[extentX/extentY 로 중심 이동]──▶ 미터 오프셋 ──[geo]──▶ 위경도\n *\n * `width`/`depth` 라 부르지 않는 이유: 컴포넌트 속성의 `depth` 는 높이 축(z)이다. 같은 화면에서\n * 한 낱말이 두 축을 가리키면 읽는 사람이 매번 어느 쪽인지 물어야 한다.\n * 단위 필드는 두지 않는다 — 미터로 고정이고, 있으면 \"단위를 지원한다\" 는 거짓 신호가 된다.\n */\n @Column({ type: 'float', nullable: true })\n @Field({ nullable: true, description: 'Extent of the design frame along the east-west axis, in metres. Area layouts are placed within this frame.' })\n extentX?: number\n\n @Column({ type: 'float', nullable: true })\n @Field({ nullable: true, description: 'Extent of the design frame along the north-south axis, in metres. Area layouts are placed within this frame.' })\n extentY?: number\n\n /**\n * 이 현장의 시각 기준 — IANA 이름(`'America/Tijuana'`).\n *\n * **테넌트가 아니라 공간이 갖는다.** 한 테넌트가 Rosarito(태평양)와 한국 공장을 함께 가질 수 있고,\n * 테넌트 단위(`Domain.timezone`)로 두면 둘 중 하나는 반드시 틀린다. 교대가 \"06시\" 라고 할 때 그것이\n * 어느 06시인가는 **물리 현장의 성질**이다.\n *\n * 오프셋 숫자가 아니라 이름으로 둔다: 숫자로는 일광절약시간을 표현할 수 없고, 나중에 절대 구간\n * (표준 `StartDateTime`)으로 옮길 때도 이름이 있어야 계산할 수 있다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'IANA time zone of this physical site (e.g. America/Tijuana) — the base for reading shift times. Belongs to the space, not the tenant, because one tenant can run plants in different zones.' })\n timezone?: string\n\n /**\n * **이 현장이 언제 도나** — 표준 `WorkCalendarEntry`(복수). 휴일 · 주말 · 정기보수가 여기 든다.\n *\n * ── 왜 공간에 있나 (2026-09-07) ──────────────────────────────────────────\n * 이 칸이 없던 동안 설비와 사람만 달력을 가질 수 있었다. 그래서 **현장이 언제 쉬는지가 어디에도\n * 없었고**, 계획 조업 시간이 「재기 시작한 뒤 흐른 시간 전부」였다 — 주말도 야간도 휴일도 계획 조업으로\n * 세었다. [산술] 8시간 × 주5일이면 168시간 중 40시간이 조업이니 그 수가 4.2배 크다.\n *\n * 시간대와 같은 축이라 같은 자리에 둔다. 「이 현장의 06:00 이 언제인가」와 「이 현장이 언제 도나」는\n * 둘 다 현장이 갖는 값이고, 테넌트가 아니다 — 한 테넌트가 다른 시간대·다른 조업일의 공장을 함께 가진다.\n *\n * `withSpaceTimeBase` 가 이 값을 모델에 얹고, 거기서 OEE 누적기가 계획 정지를 낸다. 얹는 자리를\n * 시간대와 하나로 둔 이유는 **한쪽만 채워진 상태**를 만들지 않기 위해서다.\n *\n * 없으면 아무것도 가정하지 않는다 — 종일 가동으로 보는 것은 「제약이 없다」가 아니라 「선언되지\n * 않았다」이고, 그 사실은 세어져야 한다(안 그러면 사람이 수를 되살리려고 아무 달력이나 채운다).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, {\n nullable: true,\n description:\n 'When this site runs — standard WorkCalendarEntry list (holidays, weekends, planned maintenance). Absent means undeclared, not unrestricted: planned busy time then counts the whole window.'\n })\n workCalendar?: any\n\n /**\n * 현장의 **우편 주소** — 사람이 읽고 서류에 적는 값.\n *\n * ── 왜 좌표와 따로 두나 (2026-09-07) ────────────────────────────────────\n * `geo` 는 도면을 지구에 붙이는 값이고 화면이 쓴다. 주소는 **사람과 서류의 값**이다 — 납품·통관·\n * 감사에 그 문자열이 그대로 들어가고, 좌표에서 되돌려 만들 수 없다(같은 좌표에 여러 주소가 있고\n * 동·호가 좌표에 없다).\n *\n * 고객이 준 것을 그대로 적는다. 우편번호·동·호를 우리가 지어내거나 좌표에서 유추하지 않는다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Postal address of this site as the customer states it. Kept apart from `geo`: a floor or unit number has no coordinate, and paperwork carries this string verbatim.' })\n address?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the primary representation shown on the home/monitor.' })\n primaryRepresentationId?: string\n\n /**\n * 이 **현장**의 로컬 선언 겹 — 요금처럼 **트윈이 아니라 현장이 정하는 수**.\n *\n * ── 왜 트윈에 두면 안 되나 (2026-08-18) ────────────────────────────────────\n * 요금 단가·통화는 그 현장의 계약이므로, 같은 수전을 보는 EMS 트윈이 둘이면 **같은 값을 두 번**\n * 선언해야 했다. 두 번 적는 값은 반드시 갈라진다(하나만 고치는 날이 온다) — 그때 두 트윈이 다른\n * 금액을 말하고, 어느 쪽이 맞는지 화면으로는 알 수 없다.\n *\n * 적용 순서는 **현장 먼저, 트윈이 나중**이다. 더 좁은 선언이 이긴다 — 한 현장 안에서 특정 트윈만\n * 다른 값으로 실험하는 일이 있고(예: 인상된 단가로 what-if), 그때 현장 값을 고치게 만들면 나머지\n * 트윈이 함께 흔들린다.\n *\n * 모양은 트윈 겹과 같다(`LocalDeclaration`) — 같은 규칙(검사·기억·철회)을 두 벌로 만들지 않는다.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: \"Locally-declared values that belong to this facility rather than to one twin (tariff, currency). Applied to every twin in the space before the twin's own declarations, which win when both name the same property.\" })\n localDeclarations?: any\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the space was created.' })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the space was last updated.' })\n updatedAt?: Date\n}\n"]}
@@ -10,8 +10,10 @@
10
10
  * location 자리
11
11
  * equipment 설비
12
12
  * task 작업
13
+ * transaction 거래(PO/SO/생산오더) — 오더와 다르다. 전에는 오더 칸에 섞여 들어와 한 오더의
14
+ * 이력이 두 쪽으로 갈라졌다(2026-09-05)
13
15
  */
14
- export declare const SUBJECT_KINDS: readonly ["item", "lot", "order", "location", "equipment", "task"];
16
+ export declare const SUBJECT_KINDS: readonly ["item", "lot", "order", "location", "equipment", "task", "transaction"];
15
17
  export type SubjectKind = (typeof SUBJECT_KINDS)[number];
16
18
  export interface EventSubject {
17
19
  kind: SubjectKind;
@@ -49,8 +49,10 @@ const twin_event_keys_js_1 = require("../twin-event/twin-event-keys.js");
49
49
  * location 자리
50
50
  * equipment 설비
51
51
  * task 작업
52
+ * transaction 거래(PO/SO/생산오더) — 오더와 다르다. 전에는 오더 칸에 섞여 들어와 한 오더의
53
+ * 이력이 두 쪽으로 갈라졌다(2026-09-05)
52
54
  */
53
- exports.SUBJECT_KINDS = ['item', 'lot', 'order', 'location', 'equipment', 'task'];
55
+ exports.SUBJECT_KINDS = ['item', 'lot', 'order', 'location', 'equipment', 'task', 'transaction'];
54
56
  /** 컬럼 길이 상한 — `twin-event-keys` 와 같은 값이다(같은 표에 나란히 서는 값이므로). */
55
57
  const MAX_ID = 255;
56
58
  function put(out, kind, raw) {
@@ -116,6 +118,11 @@ function subjectsOf(envelope, opts) {
116
118
  put(out, 'item', item || classEpc);
117
119
  }
118
120
  put(out, 'order', keys.orderId);
121
+ /*
122
+ * 거래는 오더가 아니다 — 갈라 적는다. 합쳐 적으면 EPCIS 사건의 GDTI 가 오더 이름으로 앉고,
123
+ * 그러면 한 오더의 이력이 두 쪽이 된다(`twin-event-keys.ts` 의 그 실측).
124
+ */
125
+ put(out, 'transaction', keys.bizTransaction);
119
126
  put(out, 'location', keys.locationId);
120
127
  put(out, 'equipment', keys.moverId);
121
128
  const data = envelope?.data;
@@ -1 +1 @@
1
- {"version":3,"file":"event-subjects.js","sourceRoot":"","sources":["../../../server/service/twin-subject/event-subjects.ts"],"names":[],"mappings":";;;AA2FA,gCA+DC;AA1JD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,yEAAgE;AAEhE;;;;;;;;;;;;GAYG;AACU,QAAA,aAAa,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,CAAU,CAAA;AAqB/F,gEAAgE;AAChE,MAAM,MAAM,GAAG,GAAG,CAAA;AAElB,SAAS,GAAG,CAAC,GAAmB,EAAE,IAAiB,EAAE,GAAY;IAC/D,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,KAAK,IAAI;QAAE,OAAM;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;IAC7B,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM;QAAE,OAAM;IACrC,sDAAsD;IACtD,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;QAAE,OAAM;IACzD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAA;AACxB,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,UAAU,CAAC,QAAa,EAAE,IAAgC;IACxE,MAAM,IAAI,GAAG,IAAA,kCAAa,EAAC,QAAQ,CAAC,CAAA;IACpC,MAAM,GAAG,GAAmB,EAAE,CAAA;IAE9B;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,GAAG,QAAQ,EAAE,IAAI,IAAI,QAAQ,IAAI,EAAE,CAAA;IAC1C,MAAM,WAAW,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAA;IAChD,MAAM,QAAQ,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAA;IAE9C,IAAI,WAAW,EAAE,CAAC;QAChB,iCAAiC;QACjC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,CAAA;QAC5B;;;WAGG;QACH,MAAM,IAAI,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAA;QAChD,IAAI,IAAI,IAAI,IAAI,KAAK,WAAW;YAAE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;IAC1D,CAAC;IACD,IAAI,QAAQ,EAAE,CAAC;QACb;;;;;WAKG;QACH,MAAM,IAAI,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,IAAI,CAAA;QACrD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,IAAI,QAAQ,CAAC,CAAA;IACpC,CAAC;IACD,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/B,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;IACrC,GAAG,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;IAEnC,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,CAAA;IAC3B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAA;IAC9B;;;;;;;;;OASG;IACH,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;IACpC,GAAG,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,CAAA;IAElC,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/*\n * **이 사건은 무엇에 대한 것인가** — 사건 하나에서 가리키는 대상들을 뽑는다.\n *\n * ── 왜 필요한가 (2026-08-27 실측) ───────────────────────────────────────────\n * 360 화면이 「이 대상의 이력」을 묻는다. 그 조회가 64초다. 대상으로 걸러 시각순으로 정렬하는 목차가\n * 없기 때문이고, 목차를 얹어도 풀리지 않는다.\n *\n * 부분일치(지금 화면) 63.998초 앞에 와일드카드가 붙어 목차를 쓸 수 없다\n * 정확일치로 바꾸면 67.316초 · 0건 품목 하나에 로트가 여럿이라 성립하지 않는다\n * 앞부분 범위로 바꾸면 68.402초 목차는 쓰지만 정렬 때문에 맞는 행을 다 가져온다\n *\n * 저장된 식별자에 품목과 로트가 한 문자열로 붙어 있는 것이 그 원인이다.\n *\n * https://seunghwa.net/class/product-M0000618-lot-L2604100071\n *\n * 그래서 문자열을 자르는 대신 **대상마다 한 줄**을 적는다. 같은 사건이 품목과 로트를 함께 가리키면\n * 두 줄이다. 자를 규칙이 필요 없다 — 넣는 쪽이 이미 둘을 안다.\n *\n * ── 왜 「뜻이 달라지는 키 칸」이 아닌가 ─────────────────────────────────────\n * `key01~05` 처럼 칸을 두고 사건 종류에 따라 뜻을 달리 쓰는 방식을 **이 표에는** 쓰지 않는다.\n *\n * 그 방식 자체가 틀린 것은 아니다. 데이터셋마다 정의가 따로 있는 프레임워크에서는 합리적이다 — 읽는\n * 쪽이 그 정의를 보고 해석한다. 우리 저널은 표 하나를 화면 여럿이 읽고 볼 정의가 없다. 그래서\n * 「M0000438 을 찾아 줘」가 어떤 줄에서는 품목, 다른 줄에서는 오더가 되고, 묻는 쪽이 사건 종류마다\n * 어느 칸에 무엇이 들어가는지 외워야 한다.\n *\n * **종류를 값으로 밝힌다.** 그러면 읽는 쪽이 사건 종류표를 들고 있지 않아도 된다.\n *\n * ── 왜 순수 모듈인가 ────────────────────────────────────────────────────────\n * 기록 경로에서 불리므로 DB·데코레이터를 알지 않는다. 그래야 테스트가 값으로 확인할 수 있다\n * (`twin-event-keys` 와 같은 이유다).\n *\n * 설계: `operato-twin/design/plans/read-models.md`\n */\nimport { twinEventKeys } from '../twin-event/twin-event-keys.js'\n\n/**\n * 대상의 종류 — **닫힌 목록**이다.\n *\n * 열어 두면 넣는 쪽마다 다른 이름을 쓰게 되고(설비를 세 가지 이름으로 부르는 식), 그러면 조회가\n * 어느 이름으로 물어야 하는지 알 수 없다. 늘려야 하면 이 목록에 더한다.\n *\n * item 선언된 품목. 로트 여럿이 같은 품목을 가리킨다\n * lot 개체 하나. 저장된 식별자 그대로다\n * order 오더\n * location 자리\n * equipment 설비\n * task 작업\n */\nexport const SUBJECT_KINDS = ['item', 'lot', 'order', 'location', 'equipment', 'task'] as const\nexport type SubjectKind = (typeof SUBJECT_KINDS)[number]\n\nexport interface EventSubject {\n kind: SubjectKind\n id: string\n}\n\n/**\n * 개체 식별자에서 **선언된 품목**을 찾아 주는 함수 — 부르는 쪽이 넘긴다.\n *\n * 이 모듈이 문자열을 자르지 않는 이유는 그 규칙이 연결된 시스템마다 다르기 때문이다. 어느 부호가\n * 로트를 가리키는지 커널 계약이 정한 적이 없고, chef 의 `-lot-` 을 여기에 굳히면 이 파일이 그\n * 시스템 위에 서게 된다(`item-ref.ts` 가 같은 판단을 적어 두었다).\n *\n * 그래서 판정은 **그 트윈의 선언**과 견주어 하고(`resolveByDeclaration`), 그 선언을 아는 것은\n * 부르는 쪽이다. 답하지 못하면 `undefined` — 그때는 품목 줄을 만들지 않는다. 짐작으로 이으면 남의\n * 품목 이력에 이 사건이 섞인다.\n */\nexport type ItemResolver = (epc: string) => string | null | undefined\n\n/** 컬럼 길이 상한 — `twin-event-keys` 와 같은 값이다(같은 표에 나란히 서는 값이므로). */\nconst MAX_ID = 255\n\nfunction put(out: EventSubject[], kind: SubjectKind, raw: unknown): void {\n if (raw === undefined || raw === null) return\n const id = String(raw).trim()\n if (!id || id.length > MAX_ID) return\n /* 같은 대상을 두 번 적지 않는다 — 사건 하나에 같은 자리가 두 번 실려 올 수 있다. */\n if (out.some(s => s.kind === kind && s.id === id)) return\n out.push({ kind, id })\n}\n\n/**\n * 사건 하나가 가리키는 대상들 — 없으면 빈 배열이다.\n *\n * 승격된 검색 키(`twinEventKeys`)를 그대로 쓴다. 그 함수가 이미 사건 종류별로 어디에서 값을 꺼낼지\n * 알고 있으므로, 여기서 다시 판단하면 두 곳이 어긋난다.\n *\n * **작업은 승격 키에 없어서 봉투에서 직접 읽는다.** 승격 컬럼을 늘리는 것은 이 작업의 범위가 아니다\n * (그것이 바로 이 파생 표로 대신하려는 일이다).\n */\nexport function subjectsOf(envelope: any, opts?: { itemOf?: ItemResolver }): EventSubject[] {\n const keys = twinEventKeys(envelope)\n const out: EventSubject[] = []\n\n /*\n * ── 개체로 온 것과 클래스로 온 것을 갈라 적는다 (2026-08-27 실측) ────────────\n *\n * `epcOf` 는 셋을 한 칸에 담는다 — `epcList[0]` · `parentID` · `quantityList[0].epcClass`.\n * 승격 컬럼이 하나라서 그렇게 되어 있고, 그 칸으로는 「개체인가 클래스인가」를 알 수 없다.\n *\n * 실측한 chef 의 사건 하나가 그 경우다. 물건을 개체로 세지 않고 무게로 담는 현장이라 `epcList` 가\n * 비어 있고 `quantityList` 에 클래스가 있다.\n *\n * epcList []\n * quantityList [{ epcClass: 'https://…/product-M0000879', quantity: 11797.6, uom: 'KGM' }]\n *\n * 그것을 로트로 적으면 뜻이 틀린다 — 개체가 아니라 품목이다. 이 표는 종류를 값으로 밝히므로\n * 갈라 적을 수 있다.\n */\n const d = envelope?.data ?? envelope ?? {}\n const instanceEpc = d.epcList?.[0] ?? d.parentID\n const classEpc = d.quantityList?.[0]?.epcClass\n\n if (instanceEpc) {\n /* 개체는 저장된 식별자 그대로다 — 자르지 않는다. */\n put(out, 'lot', instanceEpc)\n /*\n * 품목은 선언과 견주어 찾는다. 답이 개체 식별자와 같으면 품목을 찾지 못한 것이므로 줄을 만들지\n * 않는다 — `itemRefOf` 는 URI 세계에서 문자열을 그대로 돌려주기 때문에 그 값이 그대로 올 수 있다.\n */\n const item = opts?.itemOf?.(String(instanceEpc))\n if (item && item !== instanceEpc) put(out, 'item', item)\n }\n if (classEpc) {\n /*\n * 클래스로 온 것은 개체 줄을 만들지 않는다 — 그 사건에 개체가 없다. 품목 줄만 만든다.\n *\n * 선언과 견주어 품목을 찾고, 찾지 못하면 클래스 문자열을 그대로 품목으로 쓴다. 클래스는 이미 품목을\n * 가리키는 값이므로 그것이 짐작이 아니다(개체 식별자를 그대로 품목으로 쓰는 것과 다르다).\n */\n const item = opts?.itemOf?.(String(classEpc)) ?? null\n put(out, 'item', item || classEpc)\n }\n put(out, 'order', keys.orderId)\n put(out, 'location', keys.locationId)\n put(out, 'equipment', keys.moverId)\n\n const data = envelope?.data\n put(out, 'task', data?.taskId)\n /*\n * ── 작업이 오간 두 자리도 적는다 ────────────────────────────────────────────\n *\n * 승격 컬럼(`locationId`)은 EPCIS 자리만 담는다(`readPoint` · `bizLocation` · `location`). 작업이\n * A 에서 B 로 갈 때 그 컬럼에는 **둘 중 하나만** 넣을 수 있어서 아무것도 넣지 않는다.\n *\n * 여기서는 둘 다 적는다. 이것이 승격 컬럼과의 어긋남이 아니라 **이 표가 할 수 있는 일**이다 —\n * 자리 하나에 줄 하나이므로 두 자리를 함께 담는다. 「그 자리에서 무슨 일이 있었나」를 묻는 화면은\n * 출발지와 도착지를 둘 다 그 자리의 사건으로 본다.\n */\n put(out, 'location', data?.fromNode)\n put(out, 'location', data?.toNode)\n\n return out\n}\n"]}
1
+ {"version":3,"file":"event-subjects.js","sourceRoot":"","sources":["../../../server/service/twin-subject/event-subjects.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **이 사건은 무엇에 대한 것인가** — 사건 하나에서 가리키는 대상들을 뽑는다.\n *\n * ── 왜 필요한가 (2026-08-27 실측) ───────────────────────────────────────────\n * 360 화면이 「이 대상의 이력」을 묻는다. 그 조회가 64초다. 대상으로 걸러 시각순으로 정렬하는 목차가\n * 없기 때문이고, 목차를 얹어도 풀리지 않는다.\n *\n * 부분일치(지금 화면) 63.998초 앞에 와일드카드가 붙어 목차를 쓸 수 없다\n * 정확일치로 바꾸면 67.316초 · 0건 품목 하나에 로트가 여럿이라 성립하지 않는다\n * 앞부분 범위로 바꾸면 68.402초 목차는 쓰지만 정렬 때문에 맞는 행을 다 가져온다\n *\n * 저장된 식별자에 품목과 로트가 한 문자열로 붙어 있는 것이 그 원인이다.\n *\n * https://seunghwa.net/class/product-M0000618-lot-L2604100071\n *\n * 그래서 문자열을 자르는 대신 **대상마다 한 줄**을 적는다. 같은 사건이 품목과 로트를 함께 가리키면\n * 두 줄이다. 자를 규칙이 필요 없다 — 넣는 쪽이 이미 둘을 안다.\n *\n * ── 왜 「뜻이 달라지는 키 칸」이 아닌가 ─────────────────────────────────────\n * `key01~05` 처럼 칸을 두고 사건 종류에 따라 뜻을 달리 쓰는 방식을 **이 표에는** 쓰지 않는다.\n *\n * 그 방식 자체가 틀린 것은 아니다. 데이터셋마다 정의가 따로 있는 프레임워크에서는 합리적이다 — 읽는\n * 쪽이 그 정의를 보고 해석한다. 우리 저널은 표 하나를 화면 여럿이 읽고 볼 정의가 없다. 그래서\n * 「M0000438 을 찾아 줘」가 어떤 줄에서는 품목, 다른 줄에서는 오더가 되고, 묻는 쪽이 사건 종류마다\n * 어느 칸에 무엇이 들어가는지 외워야 한다.\n *\n * **종류를 값으로 밝힌다.** 그러면 읽는 쪽이 사건 종류표를 들고 있지 않아도 된다.\n *\n * ── 왜 순수 모듈인가 ────────────────────────────────────────────────────────\n * 기록 경로에서 불리므로 DB·데코레이터를 알지 않는다. 그래야 테스트가 값으로 확인할 수 있다\n * (`twin-event-keys` 와 같은 이유다).\n *\n * 설계: `operato-twin/design/plans/read-models.md`\n */\nimport { twinEventKeys } from '../twin-event/twin-event-keys.js'\n\n/**\n * 대상의 종류 — **닫힌 목록**이다.\n *\n * 열어 두면 넣는 쪽마다 다른 이름을 쓰게 되고(설비를 세 가지 이름으로 부르는 식), 그러면 조회가\n * 어느 이름으로 물어야 하는지 알 수 없다. 늘려야 하면 이 목록에 더한다.\n *\n * item 선언된 품목. 로트 여럿이 같은 품목을 가리킨다\n * lot 개체 하나. 저장된 식별자 그대로다\n * order 오더\n * location 자리\n * equipment 설비\n * task 작업\n * transaction 거래(PO/SO/생산오더) — 오더와 다르다. 전에는 오더 칸에 섞여 들어와 한 오더의\n * 이력이 두 쪽으로 갈라졌다(2026-09-05)\n */\nexport const SUBJECT_KINDS = ['item', 'lot', 'order', 'location', 'equipment', 'task', 'transaction'] as const\nexport type SubjectKind = (typeof SUBJECT_KINDS)[number]\n\nexport interface EventSubject {\n kind: SubjectKind\n id: string\n}\n\n/**\n * 개체 식별자에서 **선언된 품목**을 찾아 주는 함수 — 부르는 쪽이 넘긴다.\n *\n * 이 모듈이 문자열을 자르지 않는 이유는 그 규칙이 연결된 시스템마다 다르기 때문이다. 어느 부호가\n * 로트를 가리키는지 커널 계약이 정한 적이 없고, chef 의 `-lot-` 을 여기에 굳히면 이 파일이 그\n * 시스템 위에 서게 된다(`item-ref.ts` 가 같은 판단을 적어 두었다).\n *\n * 그래서 판정은 **그 트윈의 선언**과 견주어 하고(`resolveByDeclaration`), 그 선언을 아는 것은\n * 부르는 쪽이다. 답하지 못하면 `undefined` — 그때는 품목 줄을 만들지 않는다. 짐작으로 이으면 남의\n * 품목 이력에 이 사건이 섞인다.\n */\nexport type ItemResolver = (epc: string) => string | null | undefined\n\n/** 컬럼 길이 상한 — `twin-event-keys` 와 같은 값이다(같은 표에 나란히 서는 값이므로). */\nconst MAX_ID = 255\n\nfunction put(out: EventSubject[], kind: SubjectKind, raw: unknown): void {\n if (raw === undefined || raw === null) return\n const id = String(raw).trim()\n if (!id || id.length > MAX_ID) return\n /* 같은 대상을 두 번 적지 않는다 — 사건 하나에 같은 자리가 두 번 실려 올 수 있다. */\n if (out.some(s => s.kind === kind && s.id === id)) return\n out.push({ kind, id })\n}\n\n/**\n * 사건 하나가 가리키는 대상들 — 없으면 빈 배열이다.\n *\n * 승격된 검색 키(`twinEventKeys`)를 그대로 쓴다. 그 함수가 이미 사건 종류별로 어디에서 값을 꺼낼지\n * 알고 있으므로, 여기서 다시 판단하면 두 곳이 어긋난다.\n *\n * **작업은 승격 키에 없어서 봉투에서 직접 읽는다.** 승격 컬럼을 늘리는 것은 이 작업의 범위가 아니다\n * (그것이 바로 이 파생 표로 대신하려는 일이다).\n */\nexport function subjectsOf(envelope: any, opts?: { itemOf?: ItemResolver }): EventSubject[] {\n const keys = twinEventKeys(envelope)\n const out: EventSubject[] = []\n\n /*\n * ── 개체로 온 것과 클래스로 온 것을 갈라 적는다 (2026-08-27 실측) ────────────\n *\n * `epcOf` 는 셋을 한 칸에 담는다 — `epcList[0]` · `parentID` · `quantityList[0].epcClass`.\n * 승격 컬럼이 하나라서 그렇게 되어 있고, 그 칸으로는 「개체인가 클래스인가」를 알 수 없다.\n *\n * 실측한 chef 의 사건 하나가 그 경우다. 물건을 개체로 세지 않고 무게로 담는 현장이라 `epcList` 가\n * 비어 있고 `quantityList` 에 클래스가 있다.\n *\n * epcList []\n * quantityList [{ epcClass: 'https://…/product-M0000879', quantity: 11797.6, uom: 'KGM' }]\n *\n * 그것을 로트로 적으면 뜻이 틀린다 — 개체가 아니라 품목이다. 이 표는 종류를 값으로 밝히므로\n * 갈라 적을 수 있다.\n */\n const d = envelope?.data ?? envelope ?? {}\n const instanceEpc = d.epcList?.[0] ?? d.parentID\n const classEpc = d.quantityList?.[0]?.epcClass\n\n if (instanceEpc) {\n /* 개체는 저장된 식별자 그대로다 — 자르지 않는다. */\n put(out, 'lot', instanceEpc)\n /*\n * 품목은 선언과 견주어 찾는다. 답이 개체 식별자와 같으면 품목을 찾지 못한 것이므로 줄을 만들지\n * 않는다 — `itemRefOf` 는 URI 세계에서 문자열을 그대로 돌려주기 때문에 그 값이 그대로 올 수 있다.\n */\n const item = opts?.itemOf?.(String(instanceEpc))\n if (item && item !== instanceEpc) put(out, 'item', item)\n }\n if (classEpc) {\n /*\n * 클래스로 온 것은 개체 줄을 만들지 않는다 — 그 사건에 개체가 없다. 품목 줄만 만든다.\n *\n * 선언과 견주어 품목을 찾고, 찾지 못하면 클래스 문자열을 그대로 품목으로 쓴다. 클래스는 이미 품목을\n * 가리키는 값이므로 그것이 짐작이 아니다(개체 식별자를 그대로 품목으로 쓰는 것과 다르다).\n */\n const item = opts?.itemOf?.(String(classEpc)) ?? null\n put(out, 'item', item || classEpc)\n }\n put(out, 'order', keys.orderId)\n /*\n * 거래는 오더가 아니다 — 갈라 적는다. 합쳐 적으면 EPCIS 사건의 GDTI 가 오더 이름으로 앉고,\n * 그러면 한 오더의 이력이 두 쪽이 된다(`twin-event-keys.ts` 의 그 실측).\n */\n put(out, 'transaction', keys.bizTransaction)\n put(out, 'location', keys.locationId)\n put(out, 'equipment', keys.moverId)\n\n const data = envelope?.data\n put(out, 'task', data?.taskId)\n /*\n * ── 작업이 오간 두 자리도 적는다 ────────────────────────────────────────────\n *\n * 승격 컬럼(`locationId`)은 EPCIS 자리만 담는다(`readPoint` · `bizLocation` · `location`). 작업이\n * A 에서 B 로 갈 때 그 컬럼에는 **둘 중 하나만** 넣을 수 있어서 아무것도 넣지 않는다.\n *\n * 여기서는 둘 다 적는다. 이것이 승격 컬럼과의 어긋남이 아니라 **이 표가 할 수 있는 일**이다 —\n * 자리 하나에 줄 하나이므로 두 자리를 함께 담는다. 「그 자리에서 무슨 일이 있었나」를 묻는 화면은\n * 출발지와 도착지를 둘 다 그 자리의 사건으로 본다.\n */\n put(out, 'location', data?.fromNode)\n put(out, 'location', data?.toNode)\n\n return out\n}\n"]}
@@ -85,7 +85,7 @@ function shapeOee(o) {
85
85
  performance: Math.round((o.performance ?? 0) * 100),
86
86
  quality: Math.round((o.quality ?? 0) * 100),
87
87
  good: o.goodCount ?? 0,
88
- scrap: o.scrapCount ?? 0
88
+ scrap: o.nonconformingCount ?? 0
89
89
  };
90
90
  }
91
91
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"entity-delta.js","sourceRoot":"","sources":["../shared/entity-delta.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).\n * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.\n *\n * ── 규율: 레퍼런스가 아는 것은 **전부** 내보낸다 ────────────────────────────\n * 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는\n * 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**\n *\n * 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 오류 없이 빠졌다\n * (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은\n * **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는\n * 것을 잊으면 아무 신호 없이 정보가 사라진다.\n *\n * 그래서 반대로 한다: **상태 객체가 가진 것을 그대로 통과**시키고, 통과시키지 **않을** 것만 이유와 함께\n * 열거한다(EXCLUDED). 새 속성은 자동으로 화면에 도달하고, 빼려면 이유를 적어야 한다.\n *\n * ── 표식은 도메인 낱말을 훔치지 않는다 ─────────────────────────────────────\n * 예전 payload 는 엔티티 종류 표식을 `kind` 로 실었다. 그런데 `kind` 는 **도메인의 낱말**이다 —\n * 무버 kind=cutter, 작업 kind=putaway, 오더 kind=workorder. 표식이 그 이름을 차지하면 도메인 속성을\n * 실을 자리가 없어진다(같은 병을 오늘 ox-timeline 의 `group` 에서도 고쳤다).\n * 그래서 표식은 `entity`(우리 이름)로 옮기고 `kind` 는 도메인에 돌려준다.\n *\n * payload 계약(씬 컴포넌트가 소비): `entity`(표식) + 그 엔티티의 모든 속성(도메인 `kind` 포함).\n * entity = location · equipment · person · asset · order · task · item,\n * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).\n */\n\nexport interface EntityDelta {\n tag: string\n data: any\n}\n\n/** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */\n/*\n * 집약 채널의 접두사 — **실제 태그는 트윈 식별자를 붙여 만든다.**\n *\n * 예전에는 이 상수가 곧 태그였다. 그런데 발행 키는 (도메인, 태그)이고, **한 공간에 트윈이 여럿**\n * 있으면(실측: 3곳 × 3개) 셋이 같은 태그로 발행해 서로 덮었다. 보드의 주문 목록판이 세 공장의 주문\n * 중 마지막에 도착한 것을 보여 준다 — 오류 없이.\n *\n * 구간 성과 태그가 이미 `__kpi__:<트윈>` 으로 트윈을 붙이고 있었다(`kpi-broadcast.ts`). 같은 규약으로 맞춘다.\n */\nexport const ORDERS_TAG_PREFIX = '__orders__'\nexport const TASKS_TAG_PREFIX = '__tasks__'\nexport const ITEMS_TAG_PREFIX = '__items__'\nexport const PERSONS_TAG_PREFIX = '__persons__'\nexport const ASSETS_TAG_PREFIX = '__assets__'\n/** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */\nexport const ENERGY_TAG_PREFIX = '__energy__'\n\n/** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */\nexport function aggregateTag(prefix: string, instanceId: string): string {\n if (!instanceId) throw new Error('aggregateTag: instanceId required — tags without instanceId overwrite other twin states across tenants')\n\n return `${prefix}:${instanceId}`\n}\n\nexport const ordersTag = (instanceId: string) => aggregateTag(ORDERS_TAG_PREFIX, instanceId)\nexport const tasksTag = (instanceId: string) => aggregateTag(TASKS_TAG_PREFIX, instanceId)\nexport const itemsTag = (instanceId: string) => aggregateTag(ITEMS_TAG_PREFIX, instanceId)\nexport const personsTag = (instanceId: string) => aggregateTag(PERSONS_TAG_PREFIX, instanceId)\nexport const assetsTag = (instanceId: string) => aggregateTag(ASSETS_TAG_PREFIX, instanceId)\nexport const energyTag = (instanceId: string) => aggregateTag(ENERGY_TAG_PREFIX, instanceId)\n\n/**\n * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**\n *\n * · `id`/`epc` — 태그로 이미 나가므로 개별 payload 에 중복해 싣지 않는다(집약 뷰에는 넣는다).\n * · `oee` — 원시 누적(idleMs 등)이 매 delta 증가해 \"무변화 생략\" 을 무력화한다. shapeOee 로 정형화해 얹는다.\n */\nconst EXCLUDED = new Set(['id', 'epc', 'oee'])\n\n/**\n * OEE 원시 비율/카운트 → 표시용 정수 퍼센트 + 카운트.\n * 원시 누적 ms(idleMs 등)는 매 delta 증가해 무변화-생략을 무력화하므로 제외(유휴 자원의 매초 재브로드캐스팅 억제).\n * oee 미측정이면 undefined.\n */\nfunction shapeOee(o: any): any {\n if (!o) return undefined\n return {\n overall: Math.round((o.overall ?? 0) * 100),\n availability: Math.round((o.availability ?? 0) * 100),\n performance: Math.round((o.performance ?? 0) * 100),\n quality: Math.round((o.quality ?? 0) * 100),\n good: o.goodCount ?? 0,\n scrap: o.scrapCount ?? 0\n }\n}\n\n/**\n * 엔티티 하나의 payload — `entity` 표식 + 나머지 전부(도메인 `kind` 포함).\n *\n * `undefined` 값은 싣지 않는다(키의 유무가 시그니처를 흔들지 않게). 값이 없는 것과 키가 없는 것은\n * 소비처에서 같으므로 안전하고, 무변화 생략이 잘 듣는다.\n */\nfunction payloadOf(entityKind: string, entity: any): any {\n /* 표식은 `entity` — 도메인의 `kind` 를 덮지 않는다. */\n const data: any = { entity: entityKind }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (EXCLUDED.has(k) || v === undefined) continue\n data[k] = v\n }\n return data\n}\n\n/**\n * 집약 뷰의 항목 — 개별 payload 와 같은 내용 + 식별자.\n * 집약은 \"전체를 한 컴포넌트가 추적\" 하는 용도라 식별자가 있어야 한다.\n */\nfunction aggregateItem(entity: any, idKey: 'id' | 'epc'): any {\n const out: any = { [idKey]: entity?.[idKey] }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (k === idKey || k === 'oee' || v === undefined) continue\n out[k] = v\n }\n const shaped = shapeOee(entity?.oee)\n if (shaped) out.oee = shaped\n return out\n}\n\n/**\n * 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 브로드캐스팅에 실으면 전달이 현장을 느리게 한다.\n *\n * **알리지 않고 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가\n * \"이게 전부\" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup\n * 이 걸려 **변한 것만** 재브로드캐스팅된다.\n */\nexport const ITEMS_AGGREGATE_LIMIT = 500\n\n/**\n * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.\n *\n * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────\n * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기\n * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —\n * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.\n *\n * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.\n *\n * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────\n * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의\n * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.\n *\n * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.\n */\nfunction energyByLocation(state: any): Map<string, Record<string, unknown>> {\n const out = new Map<string, Record<string, unknown>>()\n const points = state?.energy?.points\n if (!Array.isArray(points) || !points.length) return out\n\n /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */\n const locOf = new Map<string, string>()\n for (const e of state?.equipment ?? []) if (e?.id && e?.homeLocation) locOf.set(e.id, e.homeLocation)\n\n for (const pt of points) {\n const loc = pt?.id ? locOf.get(pt.id) : undefined\n if (!loc) continue\n const acc = out.get(loc) ?? { meters: [] as string[], samplesInWindow: 0 }\n ;(acc.meters as string[]).push(pt.id)\n acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0)\n /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */\n if (Number.isFinite(Number(pt.kW))) acc.kW = Number(acc.kW ?? 0) + Number(pt.kW)\n if (Number.isFinite(Number(pt.kWh))) acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh)\n /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */\n if (pt.atMs !== undefined) {\n const iso = new Date(Number(pt.atMs)).toISOString()\n if (!acc.measuredAt || String(acc.measuredAt) < iso) acc.measuredAt = iso\n }\n out.set(loc, acc)\n }\n return out\n}\n\n/**\n * 물품 축만 **범위를 줄일 수 있다** (2026-08-21).\n *\n * ── 왜 이 인자가 생겼나 ─────────────────────────────────────────────────────\n * 브로드캐스팅 한 번이 만드는 payload 는 엔티티 수만큼이고 그 대부분이 물품이다(실측: 엔티티 3,611 중\n * 물품 2,400 · 만드는 데 1.8ms · 문자열로 바꾸는 데 1.0ms). 바뀐 것이 물품 하나여도 전부 만들어\n * 「같다」를 확인하고 버렸다.\n *\n * `items` 를 주면 **그 태그의 물품만** 개별 payload 를 만든다. 주지 않으면(`undefined`) 전부 만든다 —\n * 부르는 쪽이 무엇이 바뀌었는지 말할 수 없을 때의 답이다.\n *\n * **집약 뷰는 범위를 받지 않는다.** 집약은 목록 전체가 한 payload 라서, 한 물품이 바뀌면 그 payload 는\n * 통째로 새로 만들어야 한다(상한 500으로 이미 묶여 있다). 자리·설비·인원·자산·작업·오더도 범위를 받지\n * 않는다 — 수가 적고, 잘못 걸러서 잃는 것이 크다(자리 점유는 물품이 떠난 자리와 도착한 자리가 함께\n * 바뀌는데 사건은 한 지점만 말한다).\n */\nexport interface EntityDeltaScope {\n /** 개별 payload 를 만들 물품 태그. 없으면 전부. */\n items?: ReadonlySet<string>\n}\n\nexport function buildEntityDeltas(state: any, instanceId: string, scope?: EntityDeltaScope): EntityDelta[] {\n const out: EntityDelta[] = []\n const locEnergy = energyByLocation(state)\n\n for (const n of state?.locations ?? []) {\n /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */\n const energy = locEnergy.get(n.id)\n out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) })\n }\n for (const m of state?.equipment ?? []) {\n /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.\n * oee 만 정형화가 필요해 따로 얹는다(나머지는 통과). */\n const data = payloadOf('equipment', m)\n const oee = shapeOee(m?.oee)\n if (oee) data.oee = oee\n out.push({ tag: m.id, data })\n }\n /* 사람 — 설비와 별개 엔티티다. 같은 통과 규칙(가진 것을 전부 내보낸다). */\n for (const p of state?.persons ?? []) {\n out.push({ tag: p.id, data: payloadOf('person', p) })\n }\n /* 물리 자산(반복사용) — 물품도 설비도 아닌 네 번째 자원. */\n for (const a of state?.assets ?? []) {\n out.push({ tag: a.id, data: payloadOf('asset', a) })\n }\n for (const o of state?.orders ?? []) {\n out.push({ tag: o.id, data: payloadOf('order', o) })\n }\n for (const t of state?.tasks ?? []) {\n out.push({ tag: t.id, data: payloadOf('task', t) })\n }\n for (const it of state?.items ?? []) {\n /* 물품의 태그는 epc(개체 식별자) — 자리·설비와 달리 id 가 아니다. */\n if (!it?.epc) continue\n /* 범위가 주어졌으면 그 안의 물품만 만든다 — 나머지는 값이 그대로이므로 보낼 것이 없다. */\n if (scope?.items && !scope.items.has(it.epc)) continue\n out.push({ tag: it.epc, data: payloadOf('item', it) })\n }\n\n /* ── 집약 뷰 ──────────────────────────────────────────────────────────────\n * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가\n * 전체를 추적하는 집약 태그를 항상 방출한다. */\n const orders = state?.orders ?? []\n out.push({ tag: ordersTag(instanceId), data: { entity: 'orders', orders: orders.map((o: any) => aggregateItem(o, 'id')) } })\n\n const tasks = state?.tasks ?? []\n out.push({ tag: tasksTag(instanceId), data: { entity: 'tasks', tasks: tasks.map((t: any) => aggregateItem(t, 'id')) } })\n\n /* 사람 집약 — 인원은 보드에 개별 배치할 수도 있지만(설비처럼), 전체를 한눈에 보는 소비처도 있다.\n * 인원을 선언하지 않은 트윈에서는 빈 배열이 나간다(없음과 모름을 구별). */\n const persons = state?.persons ?? []\n out.push({ tag: personsTag(instanceId), data: { entity: 'persons', persons: persons.map((p: any) => aggregateItem(p, 'id')) } })\n\n const assets = state?.assets ?? []\n out.push({ tag: assetsTag(instanceId), data: { entity: 'assets', assets: assets.map((a: any) => aggregateItem(a, 'id')) } })\n\n const items = state?.items ?? []\n const shown = items.slice(0, ITEMS_AGGREGATE_LIMIT)\n out.push({\n tag: itemsTag(instanceId),\n data: {\n entity: 'items',\n items: shown.map((it: any) => aggregateItem(it, 'epc')),\n total: items.length,\n /* 잘렸으면 그 사실을 말한다 — \"이게 전부\" 로 읽히면 재고를 과소 판단한다. */\n ...(items.length > shown.length ? { truncated: items.length - shown.length } : {})\n }\n })\n\n /*\n * ── 에너지 (EMS) ─────────────────────────────────────────────────────────\n *\n * 계측은 `state.energy` 에 산다 — 자원 축(`equipment`·`locations`)이 아니다. 그래서 이 매핑이 없으면\n * 보드에 놓은 계량기가 **아무 값도 받지 못한다**(컴포넌트는 있는데 영원히 빈 모습).\n *\n * 두 갈래로 낸다:\n * · **개체별** — 계량 지점 하나하나의 마지막 관측(kW·kWh·역률·시각). 태그는 그 설비 id 다.\n * · **집약** — 수요 구간·피크·계약전력. 이것은 트윈 하나의 사실이라 개체에 실을 수 없다.\n *\n * 값이 없으면 **키를 넣지 않는다**(0 으로 만들지 않는다) — 「못 읽었다」와 「0 이었다」는 다른 사실이고,\n * 그 구별이 이 트윈의 값어치다.\n */\n const energy = state?.energy\n if (energy) {\n for (const pt of energy.points ?? []) {\n if (!pt?.id) continue\n out.push({\n tag: pt.id,\n data: {\n entity: 'meter',\n ...(pt.kW !== undefined ? { kW: pt.kW } : {}),\n ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),\n ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),\n /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */\n ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),\n samplesInWindow: pt.samplesInWindow ?? 0\n }\n })\n }\n out.push({\n tag: energyTag(instanceId),\n data: {\n entity: 'energy',\n ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),\n ...(energy.open ? { open: energy.open } : {}),\n ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),\n /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다.\n 전체 수는 함께 낸다(알리지 않고 자르지 않는다). */\n recent: (energy.closed ?? []).slice(-8),\n closedTotal: energy.closedTotal ?? 0\n }\n })\n }\n\n return out\n}\n"]}
1
+ {"version":3,"file":"entity-delta.js","sourceRoot":"","sources":["../shared/entity-delta.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 트윈 State 스냅샷 → 라이브 바인딩 delta 목록 (순수 매핑 — pubsub/도메인 무관 → 단위 테스트 가능).\n * publishEntityData 가 이 목록을 엔티티별 시그니처로 dedup 후 data(tag) 채널로 발행한다.\n *\n * ── 규율: 레퍼런스가 아는 것은 **전부** 내보낸다 ────────────────────────────\n * 트윈에서 사소한 속성은 없다. 소속 구역·소속 팔레트·수량·만료처럼 곁가지로 보이는 값이 실제로는\n * 구역 롤업·적재 표현·로트 추적의 전제다. 그래서 여기서 **필드를 손으로 골라 담지 않는다.**\n *\n * 예전에는 엔티티마다 필요할 것 같은 필드만 열거했고, 그 결과 계약에 있는 것이 오류 없이 빠졌다\n * (2026-08-01 감사: 노드 parentId 누락 · 작업 전량 누락 · 물품 전량 누락). 화이트리스트 방식은\n * **계약이 자라도 화면이 따라오지 못한다** — 커널이 속성을 늘릴 때마다 이 파일을 고쳐야 하고, 고치는\n * 것을 잊으면 아무 신호 없이 정보가 사라진다.\n *\n * 그래서 반대로 한다: **상태 객체가 가진 것을 그대로 통과**시키고, 통과시키지 **않을** 것만 이유와 함께\n * 열거한다(EXCLUDED). 새 속성은 자동으로 화면에 도달하고, 빼려면 이유를 적어야 한다.\n *\n * ── 표식은 도메인 낱말을 훔치지 않는다 ─────────────────────────────────────\n * 예전 payload 는 엔티티 종류 표식을 `kind` 로 실었다. 그런데 `kind` 는 **도메인의 낱말**이다 —\n * 무버 kind=cutter, 작업 kind=putaway, 오더 kind=workorder. 표식이 그 이름을 차지하면 도메인 속성을\n * 실을 자리가 없어진다(같은 병을 오늘 ox-timeline 의 `group` 에서도 고쳤다).\n * 그래서 표식은 `entity`(우리 이름)로 옮기고 `kind` 는 도메인에 돌려준다.\n *\n * payload 계약(씬 컴포넌트가 소비): `entity`(표식) + 그 엔티티의 모든 속성(도메인 `kind` 포함).\n * entity = location · equipment · person · asset · order · task · item,\n * 그리고 집약 태그(__orders__ · __tasks__ · __items__ · __persons__ · __assets__).\n */\n\nexport interface EntityDelta {\n tag: string\n data: any\n}\n\n/** 집약 고정 태그 — 클라 provider 와 일치해야 한다(런타임 생성 엔티티는 개별 배치가 불가하므로 집약 뷰). */\n/*\n * 집약 채널의 접두사 — **실제 태그는 트윈 식별자를 붙여 만든다.**\n *\n * 예전에는 이 상수가 곧 태그였다. 그런데 발행 키는 (도메인, 태그)이고, **한 공간에 트윈이 여럿**\n * 있으면(실측: 3곳 × 3개) 셋이 같은 태그로 발행해 서로 덮었다. 보드의 주문 목록판이 세 공장의 주문\n * 중 마지막에 도착한 것을 보여 준다 — 오류 없이.\n *\n * 구간 성과 태그가 이미 `__kpi__:<트윈>` 으로 트윈을 붙이고 있었다(`kpi-broadcast.ts`). 같은 규약으로 맞춘다.\n */\nexport const ORDERS_TAG_PREFIX = '__orders__'\nexport const TASKS_TAG_PREFIX = '__tasks__'\nexport const ITEMS_TAG_PREFIX = '__items__'\nexport const PERSONS_TAG_PREFIX = '__persons__'\nexport const ASSETS_TAG_PREFIX = '__assets__'\n/** 에너지 집약 — 수요 구간·피크·계약은 **트윈 하나의 사실**이라 개체 태그가 없다. */\nexport const ENERGY_TAG_PREFIX = '__energy__'\n\n/** 이 트윈의 집약 채널 태그. 트윈 식별자가 없으면 만들지 않는다 — 덮어쓰기의 원인이었다. */\nexport function aggregateTag(prefix: string, instanceId: string): string {\n if (!instanceId) throw new Error('aggregateTag: instanceId required — tags without instanceId overwrite other twin states across tenants')\n\n return `${prefix}:${instanceId}`\n}\n\nexport const ordersTag = (instanceId: string) => aggregateTag(ORDERS_TAG_PREFIX, instanceId)\nexport const tasksTag = (instanceId: string) => aggregateTag(TASKS_TAG_PREFIX, instanceId)\nexport const itemsTag = (instanceId: string) => aggregateTag(ITEMS_TAG_PREFIX, instanceId)\nexport const personsTag = (instanceId: string) => aggregateTag(PERSONS_TAG_PREFIX, instanceId)\nexport const assetsTag = (instanceId: string) => aggregateTag(ASSETS_TAG_PREFIX, instanceId)\nexport const energyTag = (instanceId: string) => aggregateTag(ENERGY_TAG_PREFIX, instanceId)\n\n/**\n * 통과시키지 않는 필드 — **빼는 데는 이유가 필요하다.**\n *\n * · `id`/`epc` — 태그로 이미 나가므로 개별 payload 에 중복해 싣지 않는다(집약 뷰에는 넣는다).\n * · `oee` — 원시 누적(idleMs 등)이 매 delta 증가해 \"무변화 생략\" 을 무력화한다. shapeOee 로 정형화해 얹는다.\n */\nconst EXCLUDED = new Set(['id', 'epc', 'oee'])\n\n/**\n * OEE 원시 비율/카운트 → 표시용 정수 퍼센트 + 카운트.\n * 원시 누적 ms(idleMs 등)는 매 delta 증가해 무변화-생략을 무력화하므로 제외(유휴 자원의 매초 재브로드캐스팅 억제).\n * oee 미측정이면 undefined.\n */\nfunction shapeOee(o: any): any {\n if (!o) return undefined\n return {\n overall: Math.round((o.overall ?? 0) * 100),\n availability: Math.round((o.availability ?? 0) * 100),\n performance: Math.round((o.performance ?? 0) * 100),\n quality: Math.round((o.quality ?? 0) * 100),\n good: o.goodCount ?? 0,\n scrap: o.nonconformingCount ?? 0\n }\n}\n\n/**\n * 엔티티 하나의 payload — `entity` 표식 + 나머지 전부(도메인 `kind` 포함).\n *\n * `undefined` 값은 싣지 않는다(키의 유무가 시그니처를 흔들지 않게). 값이 없는 것과 키가 없는 것은\n * 소비처에서 같으므로 안전하고, 무변화 생략이 잘 듣는다.\n */\nfunction payloadOf(entityKind: string, entity: any): any {\n /* 표식은 `entity` — 도메인의 `kind` 를 덮지 않는다. */\n const data: any = { entity: entityKind }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (EXCLUDED.has(k) || v === undefined) continue\n data[k] = v\n }\n return data\n}\n\n/**\n * 집약 뷰의 항목 — 개별 payload 와 같은 내용 + 식별자.\n * 집약은 \"전체를 한 컴포넌트가 추적\" 하는 용도라 식별자가 있어야 한다.\n */\nfunction aggregateItem(entity: any, idKey: 'id' | 'epc'): any {\n const out: any = { [idKey]: entity?.[idKey] }\n for (const [k, v] of Object.entries(entity ?? {})) {\n if (k === idKey || k === 'oee' || v === undefined) continue\n out[k] = v\n }\n const shaped = shapeOee(entity?.oee)\n if (shaped) out.oee = shaped\n return out\n}\n\n/**\n * 물품 집약의 상한 — 큰 현장은 물품이 수만 개라, 전량을 매 브로드캐스팅에 실으면 전달이 현장을 느리게 한다.\n *\n * **알리지 않고 자르지 않는다**: 넘치면 `total`(전체 수)과 `truncated`(잘린 수)를 함께 실어, 소비처가\n * \"이게 전부\" 라고 오해하지 않게 한다. 개별 물품은 태그별 delta 로 따로 나가고 그쪽은 시그니처 dedup\n * 이 걸려 **변한 것만** 재브로드캐스팅된다.\n */\nexport const ITEMS_AGGREGATE_LIMIT = 500\n\n/**\n * 자리별 에너지 — **그 자리의 계량기들이 잰 것**을 그 자리 태그에 실을 수 있게 모은다.\n *\n * ── 왜 자리에도 필요한가 (2026-08-14) ───────────────────────────────────────\n * 카탈로그는 자리에도 `metered` 를 준다(수전·분기·구역 계량은 재는 자리다). 그런데 값은 계량기\n * **설비** 태그로만 나갔다. 그래서 계통도의 분기 자리에 계량 컴포넌트를 놓으면 **영원히 「—」** 였다 —\n * 능력은 「잰다」고 말하는데 값이 오지 않는 자리.\n *\n * 「이 구간이 얼마를 먹나」는 자리의 사실이고, 사람은 계통도의 자리에 붙여 본다.\n *\n * ── 계층은 여기서 문제가 되지 않는다 ────────────────────────────────────────\n * 각 자리에는 **그 자리에 붙은 계량기만** 더한다(수전 자리에는 수전 계량기, 분기 자리에는 그 분기의\n * 것). 커널이 부하를 뿌리로만 세는 것과 다른 층의 계산이다 — 여기서는 자리마다 자기 것을 말한다.\n *\n * 근거를 함께 낸다(`meters`) — 그 자리의 수가 어느 계량기에서 왔는지 되짚을 수 있어야 한다.\n */\nfunction energyByLocation(state: any): Map<string, Record<string, unknown>> {\n const out = new Map<string, Record<string, unknown>>()\n const points = state?.energy?.points\n if (!Array.isArray(points) || !points.length) return out\n\n /* 계량기 → 자리. 자리를 모르는 계량기는 자리 payload 에 실을 데가 없다(설비 태그로만 나간다). */\n const locOf = new Map<string, string>()\n for (const e of state?.equipment ?? []) if (e?.id && e?.homeLocation) locOf.set(e.id, e.homeLocation)\n\n for (const pt of points) {\n const loc = pt?.id ? locOf.get(pt.id) : undefined\n if (!loc) continue\n const acc = out.get(loc) ?? { meters: [] as string[], samplesInWindow: 0 }\n ;(acc.meters as string[]).push(pt.id)\n acc.samplesInWindow = Number(acc.samplesInWindow ?? 0) + (Number(pt.samplesInWindow) || 0)\n /* 값은 **있는 것만** 더한다 — 하나도 없으면 키를 만들지 않는다(0 은 「안 썼다」가 된다). */\n if (Number.isFinite(Number(pt.kW))) acc.kW = Number(acc.kW ?? 0) + Number(pt.kW)\n if (Number.isFinite(Number(pt.kWh))) acc.kWh = Number(acc.kWh ?? 0) + Number(pt.kWh)\n /* 나이는 **가장 최근**을 쓴다 — 자리에 계량기가 여럿이면 그중 마지막 관측이 그 자리의 나이다. */\n if (pt.atMs !== undefined) {\n const iso = new Date(Number(pt.atMs)).toISOString()\n if (!acc.measuredAt || String(acc.measuredAt) < iso) acc.measuredAt = iso\n }\n out.set(loc, acc)\n }\n return out\n}\n\n/**\n * 물품 축만 **범위를 줄일 수 있다** (2026-08-21).\n *\n * ── 왜 이 인자가 생겼나 ─────────────────────────────────────────────────────\n * 브로드캐스팅 한 번이 만드는 payload 는 엔티티 수만큼이고 그 대부분이 물품이다(실측: 엔티티 3,611 중\n * 물품 2,400 · 만드는 데 1.8ms · 문자열로 바꾸는 데 1.0ms). 바뀐 것이 물품 하나여도 전부 만들어\n * 「같다」를 확인하고 버렸다.\n *\n * `items` 를 주면 **그 태그의 물품만** 개별 payload 를 만든다. 주지 않으면(`undefined`) 전부 만든다 —\n * 부르는 쪽이 무엇이 바뀌었는지 말할 수 없을 때의 답이다.\n *\n * **집약 뷰는 범위를 받지 않는다.** 집약은 목록 전체가 한 payload 라서, 한 물품이 바뀌면 그 payload 는\n * 통째로 새로 만들어야 한다(상한 500으로 이미 묶여 있다). 자리·설비·인원·자산·작업·오더도 범위를 받지\n * 않는다 — 수가 적고, 잘못 걸러서 잃는 것이 크다(자리 점유는 물품이 떠난 자리와 도착한 자리가 함께\n * 바뀌는데 사건은 한 지점만 말한다).\n */\nexport interface EntityDeltaScope {\n /** 개별 payload 를 만들 물품 태그. 없으면 전부. */\n items?: ReadonlySet<string>\n}\n\nexport function buildEntityDeltas(state: any, instanceId: string, scope?: EntityDeltaScope): EntityDelta[] {\n const out: EntityDelta[] = []\n const locEnergy = energyByLocation(state)\n\n for (const n of state?.locations ?? []) {\n /* 에너지는 자리 payload 에 **합친다** — 같은 태그로 두 번 보내면 뒤엣것이 앞을 덮는다. */\n const energy = locEnergy.get(n.id)\n out.push({ tag: n.id, data: energy ? { ...payloadOf('location', n), ...energy } : payloadOf('location', n) })\n }\n for (const m of state?.equipment ?? []) {\n /* Mobile(location/motion) + Operable(status/held) + Processable(oee) — 한 payload 로.\n * oee 만 정형화가 필요해 따로 얹는다(나머지는 통과). */\n const data = payloadOf('equipment', m)\n const oee = shapeOee(m?.oee)\n if (oee) data.oee = oee\n out.push({ tag: m.id, data })\n }\n /* 사람 — 설비와 별개 엔티티다. 같은 통과 규칙(가진 것을 전부 내보낸다). */\n for (const p of state?.persons ?? []) {\n out.push({ tag: p.id, data: payloadOf('person', p) })\n }\n /* 물리 자산(반복사용) — 물품도 설비도 아닌 네 번째 자원. */\n for (const a of state?.assets ?? []) {\n out.push({ tag: a.id, data: payloadOf('asset', a) })\n }\n for (const o of state?.orders ?? []) {\n out.push({ tag: o.id, data: payloadOf('order', o) })\n }\n for (const t of state?.tasks ?? []) {\n out.push({ tag: t.id, data: payloadOf('task', t) })\n }\n for (const it of state?.items ?? []) {\n /* 물품의 태그는 epc(개체 식별자) — 자리·설비와 달리 id 가 아니다. */\n if (!it?.epc) continue\n /* 범위가 주어졌으면 그 안의 물품만 만든다 — 나머지는 값이 그대로이므로 보낼 것이 없다. */\n if (scope?.items && !scope.items.has(it.epc)) continue\n out.push({ tag: it.epc, data: payloadOf('item', it) })\n }\n\n /* ── 집약 뷰 ──────────────────────────────────────────────────────────────\n * 런타임에 생기고 사라지는 엔티티(오더·작업·물품)는 보드에 개별 배치할 수 없다 → 한 컴포넌트가\n * 전체를 추적하는 집약 태그를 항상 방출한다. */\n const orders = state?.orders ?? []\n out.push({ tag: ordersTag(instanceId), data: { entity: 'orders', orders: orders.map((o: any) => aggregateItem(o, 'id')) } })\n\n const tasks = state?.tasks ?? []\n out.push({ tag: tasksTag(instanceId), data: { entity: 'tasks', tasks: tasks.map((t: any) => aggregateItem(t, 'id')) } })\n\n /* 사람 집약 — 인원은 보드에 개별 배치할 수도 있지만(설비처럼), 전체를 한눈에 보는 소비처도 있다.\n * 인원을 선언하지 않은 트윈에서는 빈 배열이 나간다(없음과 모름을 구별). */\n const persons = state?.persons ?? []\n out.push({ tag: personsTag(instanceId), data: { entity: 'persons', persons: persons.map((p: any) => aggregateItem(p, 'id')) } })\n\n const assets = state?.assets ?? []\n out.push({ tag: assetsTag(instanceId), data: { entity: 'assets', assets: assets.map((a: any) => aggregateItem(a, 'id')) } })\n\n const items = state?.items ?? []\n const shown = items.slice(0, ITEMS_AGGREGATE_LIMIT)\n out.push({\n tag: itemsTag(instanceId),\n data: {\n entity: 'items',\n items: shown.map((it: any) => aggregateItem(it, 'epc')),\n total: items.length,\n /* 잘렸으면 그 사실을 말한다 — \"이게 전부\" 로 읽히면 재고를 과소 판단한다. */\n ...(items.length > shown.length ? { truncated: items.length - shown.length } : {})\n }\n })\n\n /*\n * ── 에너지 (EMS) ─────────────────────────────────────────────────────────\n *\n * 계측은 `state.energy` 에 산다 — 자원 축(`equipment`·`locations`)이 아니다. 그래서 이 매핑이 없으면\n * 보드에 놓은 계량기가 **아무 값도 받지 못한다**(컴포넌트는 있는데 영원히 빈 모습).\n *\n * 두 갈래로 낸다:\n * · **개체별** — 계량 지점 하나하나의 마지막 관측(kW·kWh·역률·시각). 태그는 그 설비 id 다.\n * · **집약** — 수요 구간·피크·계약전력. 이것은 트윈 하나의 사실이라 개체에 실을 수 없다.\n *\n * 값이 없으면 **키를 넣지 않는다**(0 으로 만들지 않는다) — 「못 읽었다」와 「0 이었다」는 다른 사실이고,\n * 그 구별이 이 트윈의 값어치다.\n */\n const energy = state?.energy\n if (energy) {\n for (const pt of energy.points ?? []) {\n if (!pt?.id) continue\n out.push({\n tag: pt.id,\n data: {\n entity: 'meter',\n ...(pt.kW !== undefined ? { kW: pt.kW } : {}),\n ...(pt.kWh !== undefined ? { kWh: pt.kWh } : {}),\n ...(pt.powerFactor !== undefined ? { powerFactor: pt.powerFactor } : {}),\n /* 마지막 표본 시각 — 계측이 끊긴 것을 화면이 스스로 알 수 있게(우리가 판정하지 않는다). */\n ...(pt.atMs !== undefined ? { measuredAt: new Date(pt.atMs).toISOString() } : {}),\n samplesInWindow: pt.samplesInWindow ?? 0\n }\n })\n }\n out.push({\n tag: energyTag(instanceId),\n data: {\n entity: 'energy',\n ...(energy.contractKW !== undefined ? { contractKW: energy.contractKW } : {}),\n ...(energy.open ? { open: energy.open } : {}),\n ...(energy.peakSince ? { peakSince: energy.peakSince } : {}),\n /* 마감된 구간은 **최근 것만** 낸다 — 하루치를 매 브로드캐스팅에 실으면 채널이 그것으로 찬다.\n 전체 수는 함께 낸다(알리지 않고 자르지 않는다). */\n recent: (energy.closed ?? []).slice(-8),\n closedTotal: energy.closedTotal ?? 0\n }\n })\n }\n\n return out\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/headless-twin",
3
- "version": "10.0.19",
3
+ "version": "10.1.0",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh <heartyoh@hatiolab.com>",
@@ -27,12 +27,12 @@
27
27
  "clean:shared": "rm -rf dist-shared tsconfig.shared.tsbuildinfo"
28
28
  },
29
29
  "dependencies": {
30
- "@operato/ops-contract": "^0.8.1",
31
- "@operato/twin-kernel": "^0.10.1",
32
- "@things-factory/auth-base": "^10.0.19",
33
- "@things-factory/cache-service": "^10.0.19",
30
+ "@operato/ops-contract": "^0.9.12",
31
+ "@operato/twin-kernel": "^0.11.7",
32
+ "@things-factory/auth-base": "^10.1.0",
33
+ "@things-factory/cache-service": "^10.1.0",
34
34
  "@things-factory/env": "^10.0.8",
35
- "@things-factory/shell": "^10.0.19"
35
+ "@things-factory/shell": "^10.1.0"
36
36
  },
37
- "gitHead": "5c170a55152cfe906fe6dbc30e6bfe474ed64122"
37
+ "gitHead": "8407b1e9d1f47f6676af144d4a33c8ac01cb49c4"
38
38
  }
@@ -1,5 +1,6 @@
1
1
  import { type CanonicalRecord, type CanonicalAggregationRecord, type CanonicalTransformationRecord, type AdapterRule, type EnergyRecord, type EnergyEquipmentRecord, type EnergyGenerationRecord, type EnergyUsagePeriodRecord, type EnergyBillRecord, type EnergyTariffBasisRecord, type EnergyGenerationPriceRecord, type EnergyGenerationPeriodRecord, type OperationalRecord, type IngestResult, type MasterDataRecord } from '@operato/ops-contract'
2
2
  import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, ingestEnergyGenerationRecords, ingestEnergyUsagePeriodRecords, ingestEnergyBillRecords, ingestEnergyTariffBasisRecords, ingestEnergyGenerationPriceRecords, ingestEnergyGenerationPeriodRecords, ingestOperationalRecords, isAggregationRecord, isTransformationRecord, isEnergyRecord, isEnergyEquipmentRecord, isEnergyGenerationRecord, isEnergyUsagePeriodRecord, isEnergyBillRecord, isEnergyTariffBasisRecord, isEnergyGenerationPriceRecord, isEnergyGenerationPeriodRecord, isOperationalRecord, isMasterDataRecord, ingestMasterData } from '@operato/ops-contract'
3
+ import { factEventTime, undatedByKind } from './event-time.js'
3
4
 
4
5
  /*
5
6
  * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).
@@ -21,15 +22,66 @@ import { ingest, ingestEnergyRecords, ingestEnergyEquipmentRecords, ingestEnergy
21
22
  * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).
22
23
  */
23
24
 
25
+ /*
26
+ * **표준 헤더 필드는 한 번만 적는다** — 세 규칙이 이것을 펼쳐 쓴다.
27
+ *
28
+ * ── 왜 뽑아냈나 (2026-09-07) ────────────────────────────────────────────────
29
+ * 이 파일은 규칙 셋을 손으로 나란히 적고 있었고, 그래서 헤더 필드를 더할 때마다 **세 곳을 다 고쳐야**
30
+ * 했다. 그 구조가 같은 결함을 네 번 만들었다.
31
+ *
32
+ * ```
33
+ * quantityList 낱개 번호 없는 자재의 잔량을 말할 길이 없었다
34
+ * ilmd 포천 재고 1,704행 전부에 소비기한이 있는데 상태에는 0건
35
+ * errorDeclaration 정정이 또 하나의 사실로 쌓였다
36
+ * bizTransactionList 저널에 칸·인덱스·질의가 다 있는데 실 시스템 연결에서 0건
37
+ * ```
38
+ *
39
+ * 네 번째는 계약을 열고도 **이 파일이 그 스펙을 안 써서** 여전히 사라지고 있었다. 인티그레이션 레인이
40
+ * 규칙 그대로 `mapRecord` 를 돌려서 찾았다 — 읽어서 맞다고 하지 않고 돌려 본 것이 잡았다.
41
+ *
42
+ * 이제 헤더 필드를 더하는 것은 **이 상수 한 줄**이고, 세 갈래에 동시에 닿는다.
43
+ *
44
+ * `action` 은 여기 없다 — `TransformationEvent` 에는 그 칸이 없다(무엇이 무엇으로 되었나에는 방향이
45
+ * 하나뿐이다). 그것만 규칙마다 적는다.
46
+ */
47
+ const TWIN_INGEST_HEADER = {
48
+ bizStep: '$.bizStep',
49
+ disposition: '$.disposition',
50
+ /*
51
+ * **정정 선언** — 「이 사건은 없던 일이 되었다」(커널 0.7.65 에서 이 자리가 열렸다).
52
+ *
53
+ * 세 값이 함께여야 뜻이 있어 묶음으로 온다. 커널이 선언 시각을 요구하고, 없으면 사건 시각으로
54
+ * 메우지 않고 이유와 함께 돌려준다 — 「언제 정정했나」와 「언제 일어났나」가 같아지면 둘을 다시
55
+ * 갈라낼 수 없다.
56
+ *
57
+ * 상태는 이것으로 흔들리지 않는다. 커널은 정정을 목록에 적고 재고를 그대로 둔다 — 무엇을 어떻게
58
+ * 되돌릴지는 도메인 판단이고, 참 재고는 그 뒤의 잔량 관측이 다시 말해 준다.
59
+ */
60
+ errorDeclaration: {
61
+ declarationTime: '$.errorDeclaration.declarationTime',
62
+ reason: '$.errorDeclaration.reason',
63
+ correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'
64
+ },
65
+ /*
66
+ * **오더 참조** — 「이 물건은 어느 오더 것인가」(표준 §7.3.5.4, 계약 0.9.11 에서 이 자리가 열렸다).
67
+ *
68
+ * 받는 쪽은 처음부터 다 서 있었다: 저널에 `biz_transaction` 칸과 인덱스(`ix_twin_event_9`), 이력추적
69
+ * 질의가 그 칸으로 거르고, 커널이 네 자리에서 그것을 채운다. 그래서 커널 안쪽에서 나는 사실만 그
70
+ * 칸을 갖고 실 시스템이 먹이는 트윈은 조용히 비었다 — 시뮬 2,492건 대 `mes-line-a` 0건.
71
+ */
72
+ bizTransactionList: '$.bizTransactionList',
73
+ readPoint: '$.readPoint',
74
+ bizLocation: '$.bizLocation'
75
+ } as const
76
+
24
77
  /** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */
25
78
  const TWIN_INGEST_RULES: AdapterRule[] = [
26
79
  {
27
80
  sourceType: 'twin',
28
81
  mapping: {
29
82
  type: 'ObjectEvent',
83
+ ...TWIN_INGEST_HEADER,
30
84
  action: '$.action',
31
- bizStep: '$.bizStep',
32
- disposition: '$.disposition',
33
85
  epc: '$.epc',
34
86
  /* 비직렬 수량 — 개체 없이 「이 자리에 이 품목이 얼마 남았다」만 오는 관측이 있다. */
35
87
  quantityList: '$.quantityList',
@@ -54,13 +106,6 @@ const TWIN_INGEST_RULES: AdapterRule[] = [
54
106
  * 상태는 이것으로 흔들리지 않는다. 커널은 정정을 목록에 적고 재고를 그대로 둔다 — 무엇을 어떻게
55
107
  * 되돌릴지는 도메인 판단이고, 참 재고는 그 뒤의 잔량 관측이 다시 말해 준다.
56
108
  */
57
- errorDeclaration: {
58
- declarationTime: '$.errorDeclaration.declarationTime',
59
- reason: '$.errorDeclaration.reason',
60
- correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'
61
- },
62
- readPoint: '$.readPoint',
63
- bizLocation: '$.bizLocation'
64
109
  }
65
110
  },
66
111
  {
@@ -68,20 +113,12 @@ const TWIN_INGEST_RULES: AdapterRule[] = [
68
113
  sourceType: 'twin-transformation',
69
114
  mapping: {
70
115
  type: 'TransformationEvent',
71
- bizStep: '$.bizStep',
72
- disposition: '$.disposition',
116
+ ...TWIN_INGEST_HEADER,
73
117
  inputEPCList: '$.inputEPCList',
74
118
  inputQuantityList: '$.inputQuantityList',
75
119
  outputEPCList: '$.outputEPCList',
76
120
  outputQuantityList: '$.outputQuantityList',
77
121
  transformationID: '$.transformationID',
78
- errorDeclaration: {
79
- declarationTime: '$.errorDeclaration.declarationTime',
80
- reason: '$.errorDeclaration.reason',
81
- correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'
82
- },
83
- readPoint: '$.readPoint',
84
- bizLocation: '$.bizLocation'
85
122
  }
86
123
  },
87
124
  {
@@ -93,24 +130,29 @@ const TWIN_INGEST_RULES: AdapterRule[] = [
93
130
  sourceType: 'twin-aggregation',
94
131
  mapping: {
95
132
  type: 'AggregationEvent',
133
+ ...TWIN_INGEST_HEADER,
96
134
  action: '$.action',
97
- bizStep: '$.bizStep',
98
- disposition: '$.disposition',
99
135
  parentID: '$.parentID',
100
136
  childEPCs: '$.childEPCs',
101
137
  /* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */
102
138
  childQuantityList: '$.childQuantityList',
103
- errorDeclaration: {
104
- declarationTime: '$.errorDeclaration.declarationTime',
105
- reason: '$.errorDeclaration.reason',
106
- correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'
107
- },
108
- readPoint: '$.readPoint',
109
- bizLocation: '$.bizLocation'
110
139
  }
111
140
  }
112
141
  ]
113
142
 
143
+ /*
144
+ * 규칙을 시험이 돌릴 수 있게 낸다 — **읽어서 맞다고 하지 않기 위해.**
145
+ *
146
+ * 오늘 이 자리가 정확히 그것으로 새고 있었다: 계약을 열고 계약 시험을 초록으로 만든 뒤에도, 이 파일이
147
+ * 그 스펙을 안 써서 값이 계속 사라졌다. 인티그레이션 레인이 이 규칙 그대로 `mapRecord` 를 돌려 봐서
148
+ * 찾았다 — 규칙이 파일 안에 갇혀 있으면 그것을 돌려 볼 방법이 없다.
149
+ *
150
+ * 복사본을 낸다. 부르는 쪽이 고쳐도 유입 경로가 바뀌지 않아야 한다.
151
+ */
152
+ export function canonicalRulesForTest(): AdapterRule[] {
153
+ return TWIN_INGEST_RULES.map(r => ({ ...r, mapping: { ...r.mapping } }))
154
+ }
155
+
114
156
  /*
115
157
  * 정규 레코드 셋(`CanonicalRecord` · `CanonicalTransformationRecord` · `CanonicalAggregationRecord`)은
116
158
  * **계약으로 옮겼다**(2026-08-30, `@operato/ops-contract` 0.2.0).
@@ -281,9 +323,16 @@ export function ingestCanonicalRecords(
281
323
  * 맞는 관측 시각이다(「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다). 값을 말하는
282
324
  * 레코드만 자기 시각을 갖는다.
283
325
  */
284
- ...(((r as any).eventTime ?? (r as any).at) !== undefined
285
- ? { eventTime: (r as any).eventTime ?? (r as any).at }
286
- : {}),
326
+ /*
327
+ * ── 세 번째로 같은 자리를 밟았다 (2026-09-06) ────────────────────────
328
+ * 여기가 `eventTime ?? at` **두 이름만** 보고 있었다. `nonconformance.disposition` 은
329
+ * `data.decidedAt` 을 싣고(contract 가 required), 그래서 ingest 시각으로 떨어졌다 —
330
+ * backfill 로 5일 지난 사실을 받으니 5일 틀린 값이 저장됐다.
331
+ *
332
+ * 이름을 하나 더 넣는 것으로는 안 끝난다. type 마다의 칸을 한 표에 두고, 못 읽은 것은
333
+ * 세어서 낸다(§`event-time.ts`).
334
+ */
335
+ ...(factEventTime(r) !== undefined ? { eventTime: factEventTime(r) } : {}),
287
336
  /* 판정은 커널이 한 곳에서 한다 — 여기서 다시 짐작하면 소비처마다 답이 달라진다. */
288
337
  sourceType: isTransformationRecord(r)
289
338
  ? 'twin-transformation'
@@ -329,8 +378,29 @@ export function ingestCanonicalRecords(
329
378
  const generationResult = energyGeneration.length
330
379
  ? ingestEnergyGenerationRecords(energyGeneration, { tenantId, defaultEventTime, ...scope })
331
380
  : { accepted: [], rejected: [] }
332
- const operationalResult = operational.length
333
- ? ingestOperationalRecords(operational, { tenantId, defaultEventTime, ...scope })
381
+ /*
382
+ * ── fact type 마다 시각 칸 이름이 다르다 (2026-09-06) ────────────────────────
383
+ * 운영 레코드의 봉투 시각은 `at` 이다. 그런데 `nonconformance.disposition` 은 자기 시각을
384
+ * **payload 의 `decidedAt`** 에 싣는다(contract 가 그 자리에 required 로 선언했다). 그래서 `at` 이
385
+ * 없고 `defaultEventTime`(= ingest 시각)으로 떨어졌다.
386
+ *
387
+ * ```
388
+ * [측정함 2026-09-06, mes-line-a backfill 뒤]
389
+ * event_time 2026-09-05 23:50:51 data.decidedAt 2026-09-01T02:50:51.832Z 5일 차이
390
+ * ```
391
+ *
392
+ * push 경로에서는 몇 초 차이라 아무도 못 알아챈다. backfill 이 그 간격을 벌려서 드러냈다 —
393
+ * backfill 이 만든 결함이 아니라 원래 있던 것이 보인 것이다.
394
+ *
395
+ * 여기서 `at` 을 채워 넣는다. 더 깊은 고침은 contract 쪽이지만(선언과 봉투가 다른 이름을 쓰는
396
+ * 비대칭), 그것은 배포를 기다려야 하고 그동안 값이 계속 틀리게 저장된다.
397
+ */
398
+ const operationalDated = operational.map(r => {
399
+ const at = factEventTime(r)
400
+ return at !== undefined && (r as any).at === undefined ? { ...(r as any), at } : r
401
+ })
402
+ const operationalResult = operationalDated.length
403
+ ? ingestOperationalRecords(operationalDated, { tenantId, defaultEventTime, ...scope })
334
404
  : { accepted: [], rejected: [] }
335
405
 
336
406
  /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */
@@ -361,7 +431,15 @@ export function ingestCanonicalRecords(
361
431
  ...master.rejected
362
432
  ],
363
433
  /* 사건이 아닌 것은 따로 낸다 — 부르는 쪽이 상태 세우는 문으로 보낸다(저널로 가지 않게). */
364
- masterData: master.accepted
434
+ masterData: master.accepted,
435
+ /*
436
+ * **시각을 못 읽어 ingest 시각으로 떨어진 것** — type 별 수.
437
+ *
438
+ * 오류가 아니다. 절대값 snapshot 은 polling 순간이 맞는 시각이라 여기 세어지는 것이 정상이다.
439
+ * 다만 그 수가 어디에도 안 남으면 `event_time == created_at` 이 유일한 단서가 되고, 그것은
440
+ * 우연히 같은 것과 구별되지 않는다.
441
+ */
442
+ undated: undatedByKind([...epcis, ...operational])
365
443
  }
366
444
  }
367
445
 
@@ -373,4 +451,12 @@ export function ingestCanonicalRecords(
373
451
  */
374
452
  export interface CanonicalIngestResult extends IngestResult {
375
453
  masterData: MasterDataRecord[]
454
+ /**
455
+ * 시각을 못 읽어 ingest 시각으로 떨어진 record 를 **type 별로** 센다.
456
+ *
457
+ * 비어 있으면 전부 자기 시각을 말한 것이다. 값이 있어도 오류는 아니다 — 절대값 snapshot 은
458
+ * polling 순간이 맞는 시각이다. 다만 그 수가 남아야 「5일 틀린 값이 저장됐다」를 나중에 찾을 수
459
+ * 있다(2026-09-06 에 그것을 backfill 로 찾았다).
460
+ */
461
+ undated: Record<string, number>
376
462
  }