@things-factory/headless-twin 10.0.18 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +10 -1
  2. package/dist-server/engine/canonical-ingest.js +110 -33
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/event-time.d.ts +25 -0
  5. package/dist-server/engine/event-time.js +98 -0
  6. package/dist-server/engine/event-time.js.map +1 -0
  7. package/dist-server/engine/index.d.ts +2 -0
  8. package/dist-server/engine/index.js +23 -0
  9. package/dist-server/engine/index.js.map +1 -1
  10. package/dist-server/engine/ingest-dedupe.d.ts +18 -6
  11. package/dist-server/engine/ingest-dedupe.js +201 -15
  12. package/dist-server/engine/ingest-dedupe.js.map +1 -1
  13. package/dist-server/engine/ingest-health.d.ts +60 -1
  14. package/dist-server/engine/ingest-health.js +43 -8
  15. package/dist-server/engine/ingest-health.js.map +1 -1
  16. package/dist-server/engine/kpi-fold.d.ts +19 -0
  17. package/dist-server/engine/kpi-fold.js +4 -1
  18. package/dist-server/engine/kpi-fold.js.map +1 -1
  19. package/dist-server/engine/kpi-query.js +23 -3
  20. package/dist-server/engine/kpi-query.js.map +1 -1
  21. package/dist-server/engine/oee-accumulator.d.ts +35 -2
  22. package/dist-server/engine/oee-accumulator.js +56 -4
  23. package/dist-server/engine/oee-accumulator.js.map +1 -1
  24. package/dist-server/engine/twin-engine.d.ts +72 -3
  25. package/dist-server/engine/twin-engine.js +118 -5
  26. package/dist-server/engine/twin-engine.js.map +1 -1
  27. package/dist-server/index.js +21 -0
  28. package/dist-server/index.js.map +1 -1
  29. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.d.ts +5 -0
  30. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js +66 -0
  31. package/dist-server/migrations/1786600000000-RenameOperatoMesAdapterType.js.map +1 -0
  32. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.d.ts +5 -0
  33. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js +79 -0
  34. package/dist-server/migrations/1786700000000-AddDispatchRetryColumns.js.map +1 -0
  35. package/dist-server/migrations/index.js +5 -1
  36. package/dist-server/migrations/index.js.map +1 -1
  37. package/dist-server/routes.js +19 -16
  38. package/dist-server/routes.js.map +1 -1
  39. package/dist-server/service/actuation/actuation-advisor.d.ts +60 -0
  40. package/dist-server/service/actuation/actuation-advisor.js +53 -0
  41. package/dist-server/service/actuation/actuation-advisor.js.map +1 -0
  42. package/dist-server/service/actuation/actuation-outcome.d.ts +52 -0
  43. package/dist-server/service/actuation/actuation-outcome.js +34 -0
  44. package/dist-server/service/actuation/actuation-outcome.js.map +1 -0
  45. package/dist-server/service/actuation/actuation-rule-resolver.d.ts +19 -0
  46. package/dist-server/service/actuation/actuation-rule-resolver.js +161 -0
  47. package/dist-server/service/actuation/actuation-rule-resolver.js.map +1 -0
  48. package/dist-server/service/actuation/actuation-rule.d.ts +55 -0
  49. package/dist-server/service/actuation/actuation-rule.js +130 -0
  50. package/dist-server/service/actuation/actuation-rule.js.map +1 -0
  51. package/dist-server/service/actuation/approval-gateway.d.ts +20 -0
  52. package/dist-server/service/actuation/approval-gateway.js +14 -0
  53. package/dist-server/service/actuation/approval-gateway.js.map +1 -0
  54. package/dist-server/service/actuation/catch-up-effects.d.ts +16 -0
  55. package/dist-server/service/actuation/catch-up-effects.js +120 -0
  56. package/dist-server/service/actuation/catch-up-effects.js.map +1 -0
  57. package/dist-server/service/actuation/command-dispatcher.d.ts +230 -0
  58. package/dist-server/service/actuation/command-dispatcher.js +269 -0
  59. package/dist-server/service/actuation/command-dispatcher.js.map +1 -0
  60. package/dist-server/service/actuation/command-store.d.ts +11 -0
  61. package/dist-server/service/actuation/command-store.js +219 -0
  62. package/dist-server/service/actuation/command-store.js.map +1 -0
  63. package/dist-server/service/actuation/dispatch-after-approval.d.ts +11 -0
  64. package/dist-server/service/actuation/dispatch-after-approval.js +73 -0
  65. package/dist-server/service/actuation/dispatch-after-approval.js.map +1 -0
  66. package/dist-server/service/actuation/effect-match.d.ts +53 -0
  67. package/dist-server/service/actuation/effect-match.js +65 -0
  68. package/dist-server/service/actuation/effect-match.js.map +1 -0
  69. package/dist-server/service/actuation/index.d.ts +25 -0
  70. package/dist-server/service/actuation/index.js +30 -0
  71. package/dist-server/service/actuation/index.js.map +1 -0
  72. package/dist-server/service/actuation/retry-plan.d.ts +63 -0
  73. package/dist-server/service/actuation/retry-plan.js +99 -0
  74. package/dist-server/service/actuation/retry-plan.js.map +1 -0
  75. package/dist-server/service/actuation/rule-evaluate.d.ts +127 -0
  76. package/dist-server/service/actuation/rule-evaluate.js +205 -0
  77. package/dist-server/service/actuation/rule-evaluate.js.map +1 -0
  78. package/dist-server/service/actuation/rule-loop.d.ts +5 -0
  79. package/dist-server/service/actuation/rule-loop.js +156 -0
  80. package/dist-server/service/actuation/rule-loop.js.map +1 -0
  81. package/dist-server/service/actuation/rule-runner.d.ts +52 -0
  82. package/dist-server/service/actuation/rule-runner.js +135 -0
  83. package/dist-server/service/actuation/rule-runner.js.map +1 -0
  84. package/dist-server/service/actuation/rule-wiring.d.ts +24 -0
  85. package/dist-server/service/actuation/rule-wiring.js +80 -0
  86. package/dist-server/service/actuation/rule-wiring.js.map +1 -0
  87. package/dist-server/service/actuation/settle-effects.d.ts +21 -0
  88. package/dist-server/service/actuation/settle-effects.js +101 -0
  89. package/dist-server/service/actuation/settle-effects.js.map +1 -0
  90. package/dist-server/service/actuation/sweep-dispatch.d.ts +37 -0
  91. package/dist-server/service/actuation/sweep-dispatch.js +125 -0
  92. package/dist-server/service/actuation/sweep-dispatch.js.map +1 -0
  93. package/dist-server/service/actuation/twin-command-resolver.d.ts +120 -0
  94. package/dist-server/service/actuation/twin-command-resolver.js +406 -0
  95. package/dist-server/service/actuation/twin-command-resolver.js.map +1 -0
  96. package/dist-server/service/actuation/twin-command.d.ts +111 -0
  97. package/dist-server/service/actuation/twin-command.js +170 -0
  98. package/dist-server/service/actuation/twin-command.js.map +1 -0
  99. package/dist-server/service/actuation/twin-now.d.ts +41 -0
  100. package/dist-server/service/actuation/twin-now.js +65 -0
  101. package/dist-server/service/actuation/twin-now.js.map +1 -0
  102. package/dist-server/service/index.d.ts +5 -2
  103. package/dist-server/service/index.js +28 -17
  104. package/dist-server/service/index.js.map +1 -1
  105. package/dist-server/service/reference/actuation-routing.d.ts +60 -0
  106. package/dist-server/service/reference/actuation-routing.js +126 -0
  107. package/dist-server/service/reference/actuation-routing.js.map +1 -0
  108. package/dist-server/service/reference/actuation-target.d.ts +65 -0
  109. package/dist-server/service/reference/actuation-target.js +94 -0
  110. package/dist-server/service/reference/actuation-target.js.map +1 -0
  111. package/dist-server/service/reference/connection-portability-resolver.d.ts +29 -0
  112. package/dist-server/service/reference/connection-portability-resolver.js +181 -0
  113. package/dist-server/service/reference/connection-portability-resolver.js.map +1 -0
  114. package/dist-server/service/reference/connection-portability.d.ts +138 -0
  115. package/dist-server/service/reference/connection-portability.js +230 -0
  116. package/dist-server/service/reference/connection-portability.js.map +1 -0
  117. package/dist-server/service/reference/fill-cadence.d.ts +17 -0
  118. package/dist-server/service/reference/fill-cadence.js +31 -0
  119. package/dist-server/service/reference/fill-cadence.js.map +1 -0
  120. package/dist-server/service/reference/fill-dedupe.d.ts +16 -0
  121. package/dist-server/service/reference/fill-dedupe.js +63 -0
  122. package/dist-server/service/reference/fill-dedupe.js.map +1 -0
  123. package/dist-server/service/reference/fill-loop.d.ts +35 -0
  124. package/dist-server/service/reference/fill-loop.js +187 -0
  125. package/dist-server/service/reference/fill-loop.js.map +1 -0
  126. package/dist-server/service/reference/fill-span.d.ts +10 -0
  127. package/dist-server/service/reference/fill-span.js +37 -0
  128. package/dist-server/service/reference/fill-span.js.map +1 -0
  129. package/dist-server/service/reference/hook-contract.d.ts +149 -22
  130. package/dist-server/service/reference/hook-contract.js +236 -23
  131. package/dist-server/service/reference/hook-contract.js.map +1 -1
  132. package/dist-server/service/reference/hook-rejected.d.ts +25 -0
  133. package/dist-server/service/reference/hook-rejected.js +48 -0
  134. package/dist-server/service/reference/hook-rejected.js.map +1 -0
  135. package/dist-server/service/reference/hook-store.d.ts +3 -0
  136. package/dist-server/service/reference/hook-store.js +28 -0
  137. package/dist-server/service/reference/hook-store.js.map +1 -0
  138. package/dist-server/service/reference/index.d.ts +4 -1
  139. package/dist-server/service/reference/index.js +5 -1
  140. package/dist-server/service/reference/index.js.map +1 -1
  141. package/dist-server/service/reference/live-cadence.d.ts +68 -0
  142. package/dist-server/service/reference/live-cadence.js +44 -0
  143. package/dist-server/service/reference/live-cadence.js.map +1 -0
  144. package/dist-server/service/reference/live-feed-lease.d.ts +15 -0
  145. package/dist-server/service/reference/live-feed-lease.js +56 -0
  146. package/dist-server/service/reference/live-feed-lease.js.map +1 -0
  147. package/dist-server/service/reference/live-ingest.d.ts +21 -0
  148. package/dist-server/service/reference/live-ingest.js +66 -0
  149. package/dist-server/service/reference/live-ingest.js.map +1 -0
  150. package/dist-server/service/reference/reference-adapter.d.ts +355 -2
  151. package/dist-server/service/reference/reference-adapter.js +19 -1
  152. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  153. package/dist-server/service/reference/reference-fill-run.d.ts +50 -0
  154. package/dist-server/service/reference/reference-fill-run.js +142 -0
  155. package/dist-server/service/reference/reference-fill-run.js.map +1 -0
  156. package/dist-server/service/reference/reference-fill.d.ts +90 -0
  157. package/dist-server/service/reference/reference-fill.js +141 -0
  158. package/dist-server/service/reference/reference-fill.js.map +1 -0
  159. package/dist-server/service/reference/reference-hook.d.ts +64 -6
  160. package/dist-server/service/reference/reference-hook.js +265 -27
  161. package/dist-server/service/reference/reference-hook.js.map +1 -1
  162. package/dist-server/service/reference/reference-link-groups.d.ts +10 -0
  163. package/dist-server/service/reference/reference-link-groups.js +15 -0
  164. package/dist-server/service/reference/reference-link-groups.js.map +1 -0
  165. package/dist-server/service/reference/reference-links.d.ts +14 -0
  166. package/dist-server/service/reference/reference-links.js +73 -0
  167. package/dist-server/service/reference/reference-links.js.map +1 -0
  168. package/dist-server/service/reference/reference-live.d.ts +1 -0
  169. package/dist-server/service/reference/reference-live.js +32 -6
  170. package/dist-server/service/reference/reference-live.js.map +1 -1
  171. package/dist-server/service/reference/reference-master.d.ts +56 -2
  172. package/dist-server/service/reference/reference-master.js +33 -3
  173. package/dist-server/service/reference/reference-master.js.map +1 -1
  174. package/dist-server/service/reference/reference-resolver.d.ts +27 -0
  175. package/dist-server/service/reference/reference-resolver.js +79 -1
  176. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  177. package/dist-server/service/twin-event/twin-event-keys.d.ts +31 -3
  178. package/dist-server/service/twin-event/twin-event-keys.js +36 -4
  179. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  180. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  181. package/dist-server/service/twin-event/twin-event.js +11 -2
  182. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  183. package/dist-server/service/twin-model/choose-recipe.d.ts +15 -0
  184. package/dist-server/service/twin-model/choose-recipe.js +33 -0
  185. package/dist-server/service/twin-model/choose-recipe.js.map +1 -0
  186. package/dist-server/service/twin-model/external-resolver.d.ts +76 -0
  187. package/dist-server/service/twin-model/external-resolver.js +109 -0
  188. package/dist-server/service/twin-model/external-resolver.js.map +1 -0
  189. package/dist-server/service/twin-model/project-structure.js +16 -3
  190. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  191. package/dist-server/service/twin-model/twin-equipment.d.ts +17 -0
  192. package/dist-server/service/twin-model/twin-equipment.js +10 -0
  193. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  194. package/dist-server/service/twin-model/twin-lineage-query.js +75 -1
  195. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  196. package/dist-server/service/twin-model/twin-model-item-query.js +94 -5
  197. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-space/twin-space.d.ts +29 -0
  200. package/dist-server/service/twin-space/twin-space.js +13 -0
  201. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  202. package/dist-server/service/twin-subject/event-subjects.d.ts +3 -1
  203. package/dist-server/service/twin-subject/event-subjects.js +8 -1
  204. package/dist-server/service/twin-subject/event-subjects.js.map +1 -1
  205. package/dist-shared/entity-delta.js +1 -1
  206. package/dist-shared/entity-delta.js.map +1 -1
  207. package/package.json +7 -7
  208. package/server/engine/canonical-ingest.ts +119 -33
  209. package/server/engine/event-time.ts +96 -0
  210. package/server/engine/index.ts +20 -0
  211. package/server/engine/ingest-dedupe.ts +203 -27
  212. package/server/engine/ingest-health.ts +91 -6
  213. package/server/engine/kpi-fold.ts +24 -1
  214. package/server/engine/kpi-query.ts +23 -4
  215. package/server/engine/oee-accumulator.ts +80 -8
  216. package/server/engine/twin-engine.ts +119 -5
  217. package/server/index.ts +23 -0
  218. package/server/migrations/1786600000000-RenameOperatoMesAdapterType.ts +66 -0
  219. package/server/migrations/1786700000000-AddDispatchRetryColumns.ts +82 -0
  220. package/server/migrations/index.ts +5 -1
  221. package/server/routes.ts +19 -20
  222. package/server/service/actuation/actuation-advisor.ts +124 -0
  223. package/server/service/actuation/actuation-outcome.ts +97 -0
  224. package/server/service/actuation/actuation-rule-resolver.ts +147 -0
  225. package/server/service/actuation/actuation-rule.ts +139 -0
  226. package/server/service/actuation/approval-gateway.ts +44 -0
  227. package/server/service/actuation/catch-up-effects.ts +152 -0
  228. package/server/service/actuation/command-dispatcher.ts +511 -0
  229. package/server/service/actuation/command-store.ts +236 -0
  230. package/server/service/actuation/dispatch-after-approval.ts +71 -0
  231. package/server/service/actuation/effect-match.ts +124 -0
  232. package/server/service/actuation/index.ts +27 -0
  233. package/server/service/actuation/retry-plan.ts +145 -0
  234. package/server/service/actuation/rule-evaluate.ts +352 -0
  235. package/server/service/actuation/rule-loop.ts +160 -0
  236. package/server/service/actuation/rule-runner.ts +216 -0
  237. package/server/service/actuation/rule-wiring.ts +84 -0
  238. package/server/service/actuation/settle-effects.ts +115 -0
  239. package/server/service/actuation/sweep-dispatch.ts +179 -0
  240. package/server/service/actuation/twin-command-resolver.ts +387 -0
  241. package/server/service/actuation/twin-command.ts +232 -0
  242. package/server/service/actuation/twin-now.ts +97 -0
  243. package/server/service/index.ts +11 -0
  244. package/server/service/reference/actuation-routing.ts +165 -0
  245. package/server/service/reference/actuation-target.ts +163 -0
  246. package/server/service/reference/connection-portability-resolver.ts +171 -0
  247. package/server/service/reference/connection-portability.ts +285 -0
  248. package/server/service/reference/fill-cadence.ts +29 -0
  249. package/server/service/reference/fill-dedupe.ts +66 -0
  250. package/server/service/reference/fill-loop.ts +153 -0
  251. package/server/service/reference/fill-span.ts +31 -0
  252. package/server/service/reference/hook-contract.ts +280 -22
  253. package/server/service/reference/hook-rejected.ts +53 -0
  254. package/server/service/reference/hook-store.ts +28 -0
  255. package/server/service/reference/index.ts +5 -1
  256. package/server/service/reference/live-cadence.ts +129 -0
  257. package/server/service/reference/live-feed-lease.ts +56 -0
  258. package/server/service/reference/live-ingest.ts +94 -0
  259. package/server/service/reference/reference-adapter.ts +392 -5
  260. package/server/service/reference/reference-fill-run.ts +165 -0
  261. package/server/service/reference/reference-fill.ts +223 -0
  262. package/server/service/reference/reference-hook.ts +355 -32
  263. package/server/service/reference/reference-link-groups.ts +21 -0
  264. package/server/service/reference/reference-links.ts +77 -0
  265. package/server/service/reference/reference-live.ts +30 -5
  266. package/server/service/reference/reference-master.ts +90 -5
  267. package/server/service/reference/reference-resolver.ts +77 -2
  268. package/server/service/twin-event/twin-event-keys.ts +37 -4
  269. package/server/service/twin-event/twin-event.ts +18 -1
  270. package/server/service/twin-model/choose-recipe.ts +31 -0
  271. package/server/service/twin-model/external-resolver.ts +161 -0
  272. package/server/service/twin-model/project-structure.ts +16 -3
  273. package/server/service/twin-model/twin-equipment.ts +22 -0
  274. package/server/service/twin-model/twin-lineage-query.ts +82 -1
  275. package/server/service/twin-model/twin-model-item-query.ts +95 -4
  276. package/server/service/twin-model/twin-model-tree-query.ts +1 -1
  277. package/server/service/twin-space/twin-space.ts +39 -0
  278. package/server/service/twin-subject/event-subjects.ts +8 -1
  279. package/shared/entity-delta.ts +1 -1
  280. package/test/ack-shape-alignment.test.ts +97 -0
  281. package/test/actuation-approval-door.test.ts +421 -0
  282. package/test/actuation-dispatch.test.ts +209 -0
  283. package/test/actuation-effect.test.ts +162 -0
  284. package/test/actuation-outcome.test.ts +152 -0
  285. package/test/actuation-rule-runner.test.ts +281 -0
  286. package/test/actuation-rule.test.ts +266 -0
  287. package/test/actuation-seam.test.ts +249 -0
  288. package/test/actuation-settle.test.ts +164 -0
  289. package/test/actuation-target.test.ts +145 -0
  290. package/test/canonical-ingest-vocabularies.test.ts +49 -2
  291. package/test/capability-mapping.test.ts +1 -1
  292. package/test/choose-recipe.test.ts +117 -0
  293. package/test/command-store-writes-columns.test.ts +126 -0
  294. package/test/connection-portability-doors.test.ts +87 -0
  295. package/test/connector-capability-declaration.test.ts +54 -4
  296. package/test/declaration-reaches-model.test.ts +43 -0
  297. package/test/dedupe-transformation.test.ts +112 -0
  298. package/test/entity-delta.test.ts +1 -1
  299. package/test/event-subjects.test.ts +2 -1
  300. package/test/external-resolver.test.ts +171 -0
  301. package/test/fill-dedupe.test.ts +59 -0
  302. package/test/fill-loop.test.ts +35 -0
  303. package/test/first-envelope.test.ts +153 -0
  304. package/test/fold-shift-dst.test.ts +110 -0
  305. package/test/home-in-area.test.ts +90 -0
  306. package/test/hook-rejected-shape.test.ts +86 -0
  307. package/test/hook-response-sum.test.ts +193 -0
  308. package/test/hook-sequence.test.ts +333 -0
  309. package/test/ingest-dedupe.test.ts +56 -0
  310. package/test/ingest-expected-quiet.test.ts +111 -0
  311. package/test/ingest-idempotent.test.ts +79 -0
  312. package/test/ingest-reconcile-four-ways.test.ts +97 -0
  313. package/test/ingest-rules-header.test.ts +100 -0
  314. package/test/ingest-window-rolls-unobserved.test.ts +72 -0
  315. package/test/intake-mapping-parity.test.ts +175 -0
  316. package/test/live-cadence.test.ts +170 -0
  317. package/test/live-feed-lease.test.ts +63 -0
  318. package/test/live-mirror-parity.test.ts +1 -1
  319. package/test/oee-accumulator.test.ts +3 -3
  320. package/test/oee-site-calendar.test.ts +146 -0
  321. package/test/order-and-transaction-split.test.ts +107 -0
  322. package/test/reference-fill.test.ts +235 -0
  323. package/test/reference-hook.test.ts +114 -23
  324. package/test/reference-links.test.ts +56 -0
  325. package/test/retry-plan.test.ts +139 -0
  326. package/test/sweep-dispatch.test.ts +191 -0
  327. package/test/twin-event-keys.test.ts +20 -6
  328. package/test/twin-model-item-db.test.ts +6 -6
  329. package/test/twin-model-tree-db.test.ts +6 -6
  330. package/tsconfig.shared.tsbuildinfo +1 -1
  331. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,5 @@
1
- import { type CanonicalRecord, type CanonicalAggregationRecord, type CanonicalTransformationRecord, type EnergyRecord, type EnergyEquipmentRecord, type OperationalRecord, type IngestResult, type MasterDataRecord } from '@operato/ops-contract';
1
+ import { type CanonicalRecord, type CanonicalAggregationRecord, type CanonicalTransformationRecord, type AdapterRule, type EnergyRecord, type EnergyEquipmentRecord, type OperationalRecord, type IngestResult, type MasterDataRecord } from '@operato/ops-contract';
2
+ export declare function canonicalRulesForTest(): AdapterRule[];
2
3
  /** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */
3
4
  export declare function ingestCanonicalRecords(records: CanonicalRecord | CanonicalAggregationRecord | CanonicalTransformationRecord | EnergyRecord | EnergyEquipmentRecord | OperationalRecord | MasterDataRecord | (CanonicalRecord | CanonicalAggregationRecord | CanonicalTransformationRecord | EnergyRecord | EnergyEquipmentRecord | OperationalRecord | MasterDataRecord)[] | undefined | null, tenantId: string, defaultEventTime: string,
4
5
  /**
@@ -22,4 +23,12 @@ scope?: {
22
23
  */
23
24
  export interface CanonicalIngestResult extends IngestResult {
24
25
  masterData: MasterDataRecord[];
26
+ /**
27
+ * 시각을 못 읽어 ingest 시각으로 떨어진 record 를 **type 별로** 센다.
28
+ *
29
+ * 비어 있으면 전부 자기 시각을 말한 것이다. 값이 있어도 오류는 아니다 — 절대값 snapshot 은
30
+ * polling 순간이 맞는 시각이다. 다만 그 수가 남아야 「5일 틀린 값이 저장됐다」를 나중에 찾을 수
31
+ * 있다(2026-09-06 에 그것을 backfill 로 찾았다).
32
+ */
33
+ undated: Record<string, number>;
25
34
  }
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.canonicalRulesForTest = canonicalRulesForTest;
3
4
  exports.ingestCanonicalRecords = ingestCanonicalRecords;
4
5
  const ops_contract_1 = require("@operato/ops-contract");
6
+ const event_time_js_1 = require("./event-time.js");
5
7
  /*
6
8
  * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).
7
9
  * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).
@@ -21,15 +23,65 @@ const ops_contract_1 = require("@operato/ops-contract");
21
23
  * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).
22
24
  * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).
23
25
  */
26
+ /*
27
+ * **표준 헤더 필드는 한 번만 적는다** — 세 규칙이 이것을 펼쳐 쓴다.
28
+ *
29
+ * ── 왜 뽑아냈나 (2026-09-07) ────────────────────────────────────────────────
30
+ * 이 파일은 규칙 셋을 손으로 나란히 적고 있었고, 그래서 헤더 필드를 더할 때마다 **세 곳을 다 고쳐야**
31
+ * 했다. 그 구조가 같은 결함을 네 번 만들었다.
32
+ *
33
+ * ```
34
+ * quantityList 낱개 번호 없는 자재의 잔량을 말할 길이 없었다
35
+ * ilmd 포천 재고 1,704행 전부에 소비기한이 있는데 상태에는 0건
36
+ * errorDeclaration 정정이 또 하나의 사실로 쌓였다
37
+ * bizTransactionList 저널에 칸·인덱스·질의가 다 있는데 실 시스템 연결에서 0건
38
+ * ```
39
+ *
40
+ * 네 번째는 계약을 열고도 **이 파일이 그 스펙을 안 써서** 여전히 사라지고 있었다. 인티그레이션 레인이
41
+ * 규칙 그대로 `mapRecord` 를 돌려서 찾았다 — 읽어서 맞다고 하지 않고 돌려 본 것이 잡았다.
42
+ *
43
+ * 이제 헤더 필드를 더하는 것은 **이 상수 한 줄**이고, 세 갈래에 동시에 닿는다.
44
+ *
45
+ * `action` 은 여기 없다 — `TransformationEvent` 에는 그 칸이 없다(무엇이 무엇으로 되었나에는 방향이
46
+ * 하나뿐이다). 그것만 규칙마다 적는다.
47
+ */
48
+ const TWIN_INGEST_HEADER = {
49
+ bizStep: '$.bizStep',
50
+ disposition: '$.disposition',
51
+ /*
52
+ * **정정 선언** — 「이 사건은 없던 일이 되었다」(커널 0.7.65 에서 이 자리가 열렸다).
53
+ *
54
+ * 세 값이 함께여야 뜻이 있어 묶음으로 온다. 커널이 선언 시각을 요구하고, 없으면 사건 시각으로
55
+ * 메우지 않고 이유와 함께 돌려준다 — 「언제 정정했나」와 「언제 일어났나」가 같아지면 둘을 다시
56
+ * 갈라낼 수 없다.
57
+ *
58
+ * 상태는 이것으로 흔들리지 않는다. 커널은 정정을 목록에 적고 재고를 그대로 둔다 — 무엇을 어떻게
59
+ * 되돌릴지는 도메인 판단이고, 참 재고는 그 뒤의 잔량 관측이 다시 말해 준다.
60
+ */
61
+ errorDeclaration: {
62
+ declarationTime: '$.errorDeclaration.declarationTime',
63
+ reason: '$.errorDeclaration.reason',
64
+ correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'
65
+ },
66
+ /*
67
+ * **오더 참조** — 「이 물건은 어느 오더 것인가」(표준 §7.3.5.4, 계약 0.9.11 에서 이 자리가 열렸다).
68
+ *
69
+ * 받는 쪽은 처음부터 다 서 있었다: 저널에 `biz_transaction` 칸과 인덱스(`ix_twin_event_9`), 이력추적
70
+ * 질의가 그 칸으로 거르고, 커널이 네 자리에서 그것을 채운다. 그래서 커널 안쪽에서 나는 사실만 그
71
+ * 칸을 갖고 실 시스템이 먹이는 트윈은 조용히 비었다 — 시뮬 2,492건 대 `mes-line-a` 0건.
72
+ */
73
+ bizTransactionList: '$.bizTransactionList',
74
+ readPoint: '$.readPoint',
75
+ bizLocation: '$.bizLocation'
76
+ };
24
77
  /** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */
25
78
  const TWIN_INGEST_RULES = [
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 = [
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 = [
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,23 +130,27 @@ const TWIN_INGEST_RULES = [
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
  ];
142
+ /*
143
+ * 규칙을 시험이 돌릴 수 있게 낸다 — **읽어서 맞다고 하지 않기 위해.**
144
+ *
145
+ * 오늘 이 자리가 정확히 그것으로 새고 있었다: 계약을 열고 계약 시험을 초록으로 만든 뒤에도, 이 파일이
146
+ * 그 스펙을 안 써서 값이 계속 사라졌다. 인티그레이션 레인이 이 규칙 그대로 `mapRecord` 를 돌려 봐서
147
+ * 찾았다 — 규칙이 파일 안에 갇혀 있으면 그것을 돌려 볼 방법이 없다.
148
+ *
149
+ * 복사본을 낸다. 부르는 쪽이 고쳐도 유입 경로가 바뀌지 않아야 한다.
150
+ */
151
+ function canonicalRulesForTest() {
152
+ return TWIN_INGEST_RULES.map(r => ({ ...r, mapping: { ...r.mapping } }));
153
+ }
113
154
  /*
114
155
  * 정규 레코드 셋(`CanonicalRecord` · `CanonicalTransformationRecord` · `CanonicalAggregationRecord`)은
115
156
  * **계약으로 옮겼다**(2026-08-30, `@operato/ops-contract` 0.2.0).
@@ -251,9 +292,16 @@ scope) {
251
292
  * 맞는 관측 시각이다(「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다). 값을 말하는
252
293
  * 레코드만 자기 시각을 갖는다.
253
294
  */
254
- ...((r.eventTime ?? r.at) !== undefined
255
- ? { eventTime: r.eventTime ?? r.at }
256
- : {}),
295
+ /*
296
+ * ── 번째로 같은 자리를 밟았다 (2026-09-06) ────────────────────────
297
+ * 여기가 `eventTime ?? at` **두 이름만** 보고 있었다. `nonconformance.disposition` 은
298
+ * `data.decidedAt` 을 싣고(contract 가 required), 그래서 ingest 시각으로 떨어졌다 —
299
+ * backfill 로 5일 지난 사실을 받으니 5일 틀린 값이 저장됐다.
300
+ *
301
+ * 이름을 하나 더 넣는 것으로는 안 끝난다. type 마다의 칸을 한 표에 두고, 못 읽은 것은
302
+ * 세어서 낸다(§`event-time.ts`).
303
+ */
304
+ ...((0, event_time_js_1.factEventTime)(r) !== undefined ? { eventTime: (0, event_time_js_1.factEventTime)(r) } : {}),
257
305
  /* 판정은 커널이 한 곳에서 한다 — 여기서 다시 짐작하면 소비처마다 답이 달라진다. */
258
306
  sourceType: (0, ops_contract_1.isTransformationRecord)(r)
259
307
  ? 'twin-transformation'
@@ -297,8 +345,29 @@ scope) {
297
345
  const generationResult = energyGeneration.length
298
346
  ? (0, ops_contract_1.ingestEnergyGenerationRecords)(energyGeneration, { tenantId, defaultEventTime, ...scope })
299
347
  : { accepted: [], rejected: [] };
300
- const operationalResult = operational.length
301
- ? (0, ops_contract_1.ingestOperationalRecords)(operational, { tenantId, defaultEventTime, ...scope })
348
+ /*
349
+ * ── fact type 마다 시각 이름이 다르다 (2026-09-06) ────────────────────────
350
+ * 운영 레코드의 봉투 시각은 `at` 이다. 그런데 `nonconformance.disposition` 은 자기 시각을
351
+ * **payload 의 `decidedAt`** 에 싣는다(contract 가 그 자리에 required 로 선언했다). 그래서 `at` 이
352
+ * 없고 `defaultEventTime`(= ingest 시각)으로 떨어졌다.
353
+ *
354
+ * ```
355
+ * [측정함 2026-09-06, mes-line-a backfill 뒤]
356
+ * event_time 2026-09-05 23:50:51 data.decidedAt 2026-09-01T02:50:51.832Z 5일 차이
357
+ * ```
358
+ *
359
+ * push 경로에서는 몇 초 차이라 아무도 못 알아챈다. backfill 이 그 간격을 벌려서 드러냈다 —
360
+ * backfill 이 만든 결함이 아니라 원래 있던 것이 보인 것이다.
361
+ *
362
+ * 여기서 `at` 을 채워 넣는다. 더 깊은 고침은 contract 쪽이지만(선언과 봉투가 다른 이름을 쓰는
363
+ * 비대칭), 그것은 배포를 기다려야 하고 그동안 값이 계속 틀리게 저장된다.
364
+ */
365
+ const operationalDated = operational.map(r => {
366
+ const at = (0, event_time_js_1.factEventTime)(r);
367
+ return at !== undefined && r.at === undefined ? { ...r, at } : r;
368
+ });
369
+ const operationalResult = operationalDated.length
370
+ ? (0, ops_contract_1.ingestOperationalRecords)(operationalDated, { tenantId, defaultEventTime, ...scope })
302
371
  : { accepted: [], rejected: [] };
303
372
  /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */
304
373
  return {
@@ -328,7 +397,15 @@ scope) {
328
397
  ...master.rejected
329
398
  ],
330
399
  /* 사건이 아닌 것은 따로 낸다 — 부르는 쪽이 상태 세우는 문으로 보낸다(저널로 가지 않게). */
331
- masterData: master.accepted
400
+ masterData: master.accepted,
401
+ /*
402
+ * **시각을 못 읽어 ingest 시각으로 떨어진 것** — type 별 수.
403
+ *
404
+ * 오류가 아니다. 절대값 snapshot 은 polling 순간이 맞는 시각이라 여기 세어지는 것이 정상이다.
405
+ * 다만 그 수가 어디에도 안 남으면 `event_time == created_at` 이 유일한 단서가 되고, 그것은
406
+ * 우연히 같은 것과 구별되지 않는다.
407
+ */
408
+ undated: (0, event_time_js_1.undatedByKind)([...epcis, ...operational])
332
409
  };
333
410
  }
334
411
  //# sourceMappingURL=canonical-ingest.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":"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","sourcesContent":["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'\nimport { 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'\n\n/*\n * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).\n * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).\n * 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.\n * 여기 도착하는 레코드는 이미 정규 필드다 — 어휘마다 자기 필드 이름이 계약이다:\n * EPCIS 품목 { epc, action, bizStep, disposition?, readPoint?, bizLocation? }\n * 에너지 계량 { meterId, kW?, kWh?, … } · 설비 에너지 { equipmentId, generatedKW?, soc?, … }\n * 발전 적산 { equipmentId, kWh, kWhSince?, … }\n * 사용 구간 { meterId, from, to, kWh, unitPrice?, basis?, … } · 청구서 { from, to, total?, … }\n * 요금 기준 { from, to, billingDemandKW?, demandChargePerKW?, currency }\n * 담김(적재) { parentID, childEPCs[] | childQuantityList[], action, bizStep, … }\n * 운영 사실 { taskId | moverId | personId | assetId | orderId, status, … } // vocabulary-guard: allow 저널 와이어 필드\n * 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).\n *\n * 두 소비처가 이 단일 코어를 공유(중복 금지):\n * - sim/scenario 경로: operato-twin twin-ingest 태스크(http-get→jsonata→여기).\n * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).\n * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).\n */\n\n/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */\nconst TWIN_INGEST_RULES: AdapterRule[] = [\n {\n sourceType: 'twin',\n mapping: {\n type: 'ObjectEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n epc: '$.epc',\n /* 비직렬 수량 — 개체 없이 「이 자리에 이 품목이 얼마 남았다」만 오는 관측이 있다. */\n quantityList: '$.quantityList',\n /*\n * **개체·로트의 마스터데이터**(로트 번호·소비기한 등) — 커널 0.7.58 에서 이 자리가 열렸다.\n *\n * 실측(포천): 원본 재고 1,704행 **전부**에 소비기한이 있는데 투영된 상태에는 **0건**이었다. 지난\n * 재고 950건·59,785kg 이 어디에도 나타나지 않았고, 빈 화면이 「이상 없음」으로 읽혔다.\n *\n * ★ **표준이 `action: 'ADD'` 일 때만 허용한다**(또는 변환). 마스터데이터는 로트가 **생길 때**\n * 말하는 사실이고, 주기 관측마다 실으면 커널이 거부한다 — 알리지 않고 통과시키지 않는 것이 옳다\n * (커넥터가 첫 목격에 `ADD`, 그 뒤로 `OBSERVE` 를 내야 한다).\n */\n ilmd: '$.ilmd',\n /*\n * **정정 선언** — 「이 사건은 없던 일이 되었다」(커널 0.7.65 에서 이 자리가 열렸다).\n *\n * 세 값이 함께여야 뜻이 있어 묶음으로 온다. 커널이 선언 시각을 요구하고, 없으면 사건 시각으로\n * 메우지 않고 이유와 함께 돌려준다 — 「언제 정정했나」와 「언제 일어났나」가 같아지면 둘을 다시\n * 갈라낼 수 없다.\n *\n * 상태는 이것으로 흔들리지 않는다. 커널은 정정을 목록에 적고 재고를 그대로 둔다 — 무엇을 어떻게\n * 되돌릴지는 도메인 판단이고, 참 재고는 그 뒤의 잔량 관측이 다시 말해 준다.\n */\n errorDeclaration: {\n declarationTime: '$.errorDeclaration.declarationTime',\n reason: '$.errorDeclaration.reason',\n correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'\n },\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n },\n {\n /* **변환의 사실** — 무엇으로 무엇이 되었나. 이름은 표준의 것을 그대로 쓴다(갈래 판정이 그 이름을 본다). */\n sourceType: 'twin-transformation',\n mapping: {\n type: 'TransformationEvent',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n inputEPCList: '$.inputEPCList',\n inputQuantityList: '$.inputQuantityList',\n outputEPCList: '$.outputEPCList',\n outputQuantityList: '$.outputQuantityList',\n transformationID: '$.transformationID',\n errorDeclaration: {\n declarationTime: '$.errorDeclaration.declarationTime',\n reason: '$.errorDeclaration.reason',\n correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'\n },\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n },\n {\n /*\n * **담김의 사실** — 무엇이 무엇에 실렸나(팔레트·상자). 오래 비어 있던 자리다: 관측 리듀서는 이 사실을\n * 받으면 자식 물품에 `parent` 를 붙이는데, 문이 `ObjectEvent` 하나만 알아서 원본은 적재를 말할 길이\n * 없었다(시뮬 원본을 붙여 돌려 보고서야 로그로 드러났다).\n */\n sourceType: 'twin-aggregation',\n mapping: {\n type: 'AggregationEvent',\n action: '$.action',\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n parentID: '$.parentID',\n childEPCs: '$.childEPCs',\n /* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */\n childQuantityList: '$.childQuantityList',\n errorDeclaration: {\n declarationTime: '$.errorDeclaration.declarationTime',\n reason: '$.errorDeclaration.reason',\n correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'\n },\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n }\n }\n]\n\n/*\n * 정규 레코드 셋(`CanonicalRecord` · `CanonicalTransformationRecord` · `CanonicalAggregationRecord`)은\n * **계약으로 옮겼다**(2026-08-30, `@operato/ops-contract` 0.2.0).\n *\n * 여기 있던 이유는 소비 방식 때문이었다 — EPCIS 는 커널의 범용 어댑터를 지나서 타입을 요구하지 않았다.\n * 그런데 모양 자체는 EPCIS 2.0 의 필드 이름이고 호스트의 것이 아니다. 이제 MES 가 이 모양 그대로\n * 보낸다.\n *\n * **매핑 룰(`TWIN_INGEST_RULES`)은 여기 남는다** — 이 모양을 EPCIS 사건으로 옮기는 정책이지 모양이 아니다.\n */\n\n/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */\nexport function ingestCanonicalRecords(\n records:\n | CanonicalRecord\n | CanonicalAggregationRecord\n | CanonicalTransformationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n /* 다섯째 어휘 — 사건이 아닌 것(변하지 않는 속성). 저널로 가지 않는다(§`CanonicalIngestResult`). */\n | MasterDataRecord\n | (\n | CanonicalRecord\n | CanonicalAggregationRecord\n | CanonicalTransformationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n | MasterDataRecord\n )[]\n | undefined\n | null,\n tenantId: string,\n defaultEventTime: string,\n /**\n * **이 사실들이 어느 트윈의 것인가** — 마감된 구간 사실의 이름에 쓰인다(커널 §`SubjectBasis`).\n *\n * 커널은 도메인으로 서므로 자기가 어느 트윈인지 모른다. 그런데 설비 번호·계량 지점 번호는 트윈\n * 안에서만 통하는 이름표라, 한 도메인에 현장이 둘이면 서로 다른 설비의 같은 날이 한 사실이 된다.\n * 실측으로 그 일이 났다 — 두 발전소의 `002` 가 겹쳤다.\n *\n * 주지 않으면 이름표를 그대로 쓰고 `twin-local` 로 표시된다 — 겹칠 수 있다는 뜻이다.\n */\n scope?: { scopeId?: string; identityOf?: (kind: 'equipment' | 'meter', localId: string) => string | undefined }\n): CanonicalIngestResult {\n const arr = Array.isArray(records) ? records : records ? [records] : []\n /*\n * ── **다섯째 어휘는 사건이 아니다** (2026-08-28) ────────────────────────────\n *\n * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호·자리의 성질). 표준은 이것을 사건이 아니라\n * **어휘 요소의 속성**으로 담고, 커널에 그 자리가 없어서 지금까지 사건에 실려 왔다.\n *\n * 그 결과가 유입에서 이렇게 났다. 전량을 읽는 원본이 유통기한을 실으려고 **들어오지 않은 것을\n * `ADD`** 라고 말했고, `ilmd` 가 사건 단위라 자리별로 묶을 수도 없어 낱개로 나갔다 — 재기동 한 번에\n * 2,665건이다.\n *\n * 그래서 여기서 갈라 낸다. 이것은 봉투가 되지 않고 저널에도 적히지 않는다(시각축이 없는 값이다).\n * 갈래 판정은 커널의 것을 부른다 — 다른 넷과 같은 규율이다.\n *\n * 설계: `operato-twin/design/plans/master-data.md`\n */\n const masterCandidates = arr.filter(isMasterDataRecord)\n const master = masterCandidates.length ? ingestMasterData(masterCandidates) : { accepted: [], rejected: [] }\n /*\n * **어휘가 넷이다** — 물류(EPCIS)·에너지 계량·설비 에너지·운영 사실. 예전에는 이 함수가 무조건\n * `ObjectEvent` 를 만들었고, 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)\n * 엉뚱한 물품 관측이 됐다.\n *\n * 물류 어휘 안에는 모양이 둘이다(개체 관측 `epc` / 담김 `parentID`+`childEPCs`).\n *\n * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`·`isOperationalRecord`·`isAggregationRecord`) — 여기서 다시 짐작하면\n * 소비처마다 답이 달라진다. 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·브로드캐스팅)는 이 갈림을 알\n * 필요가 없다.\n */\n /*\n * 여섯째·일곱째 어휘 — **마감된 사용 구간**과 **청구서**. 커널 0.7.72 가 이 문들을 열었다.\n *\n * ── ★ 사용 구간을 계량보다 **먼저** 가른다 ─────────────────────────────────\n * `isEnergyRecord` 는 `meterId` 가 있으면 참이므로 사용 구간에도 참이다. 순서를 정하지 않으면\n * 시간별 사용량이 계량 표본으로 들어가고, 그 문은 `kWh` 를 **적산 레지스터**로 읽는다. 그러면\n * 커널이 구간 전력량을 차분으로 구하는데 우리가 넣은 값이 이미 구간의 양이다 — 차분의 차분이 되어\n * 값이 조용히 틀린다. 거부되지도 않는다.\n */\n const usagePeriods = arr.filter(isEnergyUsagePeriodRecord) as unknown as EnergyUsagePeriodRecord[]\n const bills = arr.filter(isEnergyBillRecord) as unknown as EnergyBillRecord[]\n /*\n * 여덟째 어휘 — **그 주기에 적용되는 요금 기준**. 커널 0.7.76 이 이 문을 열었다.\n *\n * ★ 청구서와 겹친다. 청구서가 `billingDemandKW` 를 실을 수 있고, 그러면 두 판정이 다 참이다.\n * 그때 요금 기준 문으로 보내면 「금액은 이 문이 받지 않는다」로 거부된다 — 정산이 통째로\n * 사라진다. 그래서 청구서인 것을 뺀다. 커널에서 배타로 만들어 달라고 올렸다.\n */\n const tariffBases = arr.filter(r => isEnergyTariffBasisRecord(r) && !isEnergyBillRecord(r)) as unknown as EnergyTariffBasisRecord[]\n /*\n * 아홉째 어휘 — **발전 단가**. 낸 것에 매겨지는 1kWh 당 값이고 날마다 바뀐다(커널 0.7.79).\n *\n * 다른 문과 겹치지 않는다: 계량 지점이나 설비 id 가 있으면 이 문이 아니고(커널이 배타로 막는다),\n * 요금적용전력·기본요금 단가가 없으므로 요금 기준도 아니다.\n */\n const generationPrices = arr.filter(isEnergyGenerationPriceRecord) as unknown as EnergyGenerationPriceRecord[]\n /* 마감된 발전 기간 — 지난 기록 채우기가 쓴다. 적산의 문과 배타다(구간이 있으면 이쪽). */\n const generationPeriods = arr.filter(isEnergyGenerationPeriodRecord) as unknown as EnergyGenerationPeriodRecord[]\n const energy = arr.filter(r => isEnergyRecord(r) && !isEnergyUsagePeriodRecord(r)) as EnergyRecord[]\n /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는\n 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */\n /*\n * 다섯 번째 어휘 — **발전 적산**(설비가 지금까지 만든 양). 커널 0.7.69 가 이 문을 열었다.\n *\n * ── ★ 설비 상태보다 **먼저** 가른다 ──────────────────────────────────────────\n * 두 판정이 겹친다. `isEnergyEquipmentRecord` 는 `equipmentId` 가 있고 물류·계량 어휘가 아니면\n * 참이므로 발전 적산도 함께 참이다. 그래서 순서가 갈림을 정한다.\n *\n * 순서를 잘못 두면 조용히 사라지는 것이 아니라 **엉뚱한 이유로 거부된다**: 설비 상태 문은 적산을\n * 값으로 세지 않아 「바꿀 상태가 하나도 없다」로 돌려보낸다. 커넥터는 보냈다고 믿고, 화면에는\n * 발전량이 없다.\n */\n const energyGeneration = arr.filter(isEnergyGenerationRecord) as unknown as EnergyGenerationRecord[]\n const energyEquipment = arr.filter(r => isEnergyEquipmentRecord(r) && !isEnergyGenerationRecord(r)) as unknown as EnergyEquipmentRecord[]\n /*\n * 네 번째 어휘 — **운영 사실**(작업·설비·사람·자산·오더·품질). 관측 리듀서는 이것을 오래전부터\n * 계산했는데 **들어올 문이 없었다**: 원본이 「이 작업이 끝났다」를 말할 길이 없어 시뮬 커널만 그것을\n * 아는 상태가 남았다(같은 화면이 두 구동에서 다른 것을 말한다).\n */\n const operational = arr.filter(isOperationalRecord) as OperationalRecord[]\n /*\n * ── 이 목록은 **어휘를 더할 때마다 함께 늘어야 한다** (2026-08-30) ─────────\n *\n * EPCIS 갈래는 「나머지 전부」다. 새 어휘를 빼 주지 않으면 그 레코드가 제 문으로 가면서\n * **EPCIS 문으로도 들어가** 거부된다. 받아들여지기도 하고 거부되기도 하는 상태가 되어,\n * 유입 장부의 거부 수가 늘고 사람이 그것을 진짜 결함으로 읽는다.\n *\n * 실제로 발전 단가를 더할 때 그렇게 됐다. 목록을 손으로 지키는 대신 시험이 지킨다\n * (§`canonical-ingest-vocabularies` — 어휘마다 한 문으로만 가는지).\n */\n const epcis = arr.filter(\n r =>\n !isEnergyRecord(r) &&\n !isEnergyEquipmentRecord(r) &&\n !isEnergyGenerationRecord(r) &&\n !isEnergyUsagePeriodRecord(r) &&\n !isEnergyBillRecord(r) &&\n !isEnergyTariffBasisRecord(r) &&\n !isEnergyGenerationPriceRecord(r) &&\n !isEnergyGenerationPeriodRecord(r) &&\n !isOperationalRecord(r) &&\n !isMasterDataRecord(r)\n ) as (CanonicalRecord | CanonicalAggregationRecord | CanonicalTransformationRecord)[]\n\n /* 품목 어휘 안에서도 갈림이 있다(개체 관측 / 담김) — 그 판정도 커널의 것을 부른다. */\n const epcisResult = epcis.length\n ? ingest(\n epcis.map(r => ({\n ...r,\n /*\n * ── **시각의 이름을 둘 흡수한다** (2026-08-24 실측) ────────────────────\n * `eventTimePath` 는 **한 이름**만 받는다. 호스트가 `'eventTime'` 으로 못박아 두었는데, 뒤에\n * 생긴 커넥터는 `at` 을 싣는다(에너지 채널이 이미 `at` 을 쓰고 있어 그 어휘를 따랐다).\n * 그래서 그 커넥터의 사건은 **전부 폴링 순간**으로 찍혔다 — 저널 18,150건이 그랬다.\n *\n * 그 결과 둘이 함께 망가졌다: 재고 관측이 트윈의 시계를 밀지 못해 시계가 `task.status` 의\n * 최댓값(2026-04-15)에 **멈춰 있었고**, 이동 사건에서 「언제」가 사라져 순서만 남았다.\n *\n * 이름을 하나로 강요하지 않고 흡수한다 — `orderId`·`resourceRef`·`equipmentId` 를 함께 보는\n * 것과 같은 규율이다(§`twin-event-keys`). 세대가 섞이는 것은 이 이음새의 성질이다.\n *\n * **없으면 만들지 않는다**: 절대값 스냅샷 관측은 원본의 갱신 시각이 아니라 **폴링 순간**이\n * 맞는 관측 시각이다(「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다). 값을 말하는\n * 레코드만 자기 시각을 갖는다.\n */\n ...(((r as any).eventTime ?? (r as any).at) !== undefined\n ? { eventTime: (r as any).eventTime ?? (r as any).at }\n : {}),\n /* 판정은 커널이 한 곳에서 한다 — 여기서 다시 짐작하면 소비처마다 답이 달라진다. */\n sourceType: isTransformationRecord(r)\n ? 'twin-transformation'\n : isAggregationRecord(r)\n ? 'twin-aggregation'\n : 'twin'\n })),\n TWIN_INGEST_RULES,\n /*\n * **레코드가 시각을 말하면 그것을 쓴다** (2026-08-23).\n *\n * `eventTimePath` 를 주지 않던 동안 이 경로는 **언제나 폴링 시각**을 썼다(`defaultEventTime`).\n * 그래서 커넥터가 이미 싣고 있던 `eventTime`(변환 레코드의 `finishedAt` 등)이 **알리지 않고\n * 무시됐다** — 커넥터를 붙인 쪽은 그것이 먹는다고 믿고 있었고, 어디에도 오류가 나지 않았다.\n *\n * 무엇이 걸렸나: 계보(변환)의 시각이다. 4월에 만든 로트가 8월에 만들어진 것으로 저널에 남았다.\n * 그리고 실측 소요를 재는 폴드가 봉투 시각을 쓰므로(§`kpi-fold`: `workMs = 완료 − 첫 착수`),\n * 폴링 간격을 작업시간으로 배울 수 있었다 — 못 배우는 것보다 나쁘다.\n *\n * 레코드에 `eventTime` 이 없으면 커널이 알리지 않고 기본값으로 떨어진다(§`resolve` — 없는 값은\n * 오류가 아니다). 그래서 이 한 줄이 기존 경로를 막지 않는다: 시각을 말하는 레코드만 사실이 된다.\n */\n { tenantId, defaultEventTime, eventTimePath: 'eventTime' }\n )\n : { accepted: [], rejected: [] }\n const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime, ...scope }) : { accepted: [], rejected: [] }\n const equipmentResult = energyEquipment.length\n ? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const usageResult = usagePeriods.length\n ? ingestEnergyUsagePeriodRecords(usagePeriods, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const billResult = bills.length ? ingestEnergyBillRecords(bills, { tenantId, defaultEventTime, ...scope }) : { accepted: [], rejected: [] }\n const tariffBasisResult = tariffBases.length\n ? ingestEnergyTariffBasisRecords(tariffBases, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationPeriodResult = generationPeriods.length\n ? ingestEnergyGenerationPeriodRecords(generationPeriods, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationPriceResult = generationPrices.length\n ? ingestEnergyGenerationPriceRecords(generationPrices, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationResult = energyGeneration.length\n ? ingestEnergyGenerationRecords(energyGeneration, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const operationalResult = operational.length\n ? ingestOperationalRecords(operational, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n\n /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */\n return {\n accepted: [\n ...epcisResult.accepted,\n ...energyResult.accepted,\n ...equipmentResult.accepted,\n ...generationResult.accepted,\n ...usageResult.accepted,\n ...generationPriceResult.accepted,\n ...generationPeriodResult.accepted,\n ...billResult.accepted,\n ...tariffBasisResult.accepted,\n ...operationalResult.accepted\n ],\n rejected: [\n ...epcisResult.rejected,\n ...energyResult.rejected,\n ...equipmentResult.rejected,\n ...generationResult.rejected,\n ...usageResult.rejected,\n ...billResult.rejected,\n ...tariffBasisResult.rejected,\n ...generationPriceResult.rejected,\n ...generationPeriodResult.rejected,\n ...operationalResult.rejected,\n ...master.rejected\n ],\n /* 사건이 아닌 것은 따로 낸다 — 부르는 쪽이 상태 세우는 문으로 보낸다(저널로 가지 않게). */\n masterData: master.accepted\n }\n}\n\n/**\n * 유입 결과 — 사건(봉투)과 **사건이 아닌 것**을 갈라 낸다.\n *\n * 마스터데이터를 `accepted` 에 섞지 않는 이유: 부르는 쪽이 그것을 저널에 적게 되고, 시각축이 없는 값이\n * 「그때 일어난 일」로 남는다. 갈래를 타입으로 갈라 두면 그 실수를 할 수 없다.\n */\nexport interface CanonicalIngestResult extends IngestResult {\n masterData: MasterDataRecord[]\n}\n"]}
1
+ {"version":3,"file":"canonical-ingest.js","sourceRoot":"","sources":["../../server/engine/canonical-ingest.ts"],"names":[],"mappings":"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","sourcesContent":["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'\nimport { 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'\nimport { factEventTime, undatedByKind } from './event-time.js'\n\n/*\n * 정규 레코드 → 커널 검증 ACL → CanonicalEnvelope (P1 seam 코어).\n * 순수(커널만 의존; shell·integration-base 무의존 → node:test 검증 가능).\n * 매핑(레거시 스키마 → 정규)은 이 앞 단계(jsonata step 또는 커넥터 openLiveFeed)가 끝낸다.\n * 여기 도착하는 레코드는 이미 정규 필드다 — 어휘마다 자기 필드 이름이 계약이다:\n * EPCIS 품목 { epc, action, bizStep, disposition?, readPoint?, bizLocation? }\n * 에너지 계량 { meterId, kW?, kWh?, … } · 설비 에너지 { equipmentId, generatedKW?, soc?, … }\n * 발전 적산 { equipmentId, kWh, kWhSince?, … }\n * 사용 구간 { meterId, from, to, kWh, unitPrice?, basis?, … } · 청구서 { from, to, total?, … }\n * 요금 기준 { from, to, billingDemandKW?, demandChargePerKW?, currency }\n * 담김(적재) { parentID, childEPCs[] | childQuantityList[], action, bizStep, … }\n * 운영 사실 { taskId | moverId | personId | assetId | orderId, status, … } // vocabulary-guard: allow 저널 와이어 필드\n * 커널은 objectEvent 구성 + EPCIS 검증(위반=오염 차단) + 봉투 래핑만(방언 없음).\n *\n * 두 소비처가 이 단일 코어를 공유(중복 금지):\n * - sim/scenario 경로: operato-twin twin-ingest 태스크(http-get→jsonata→여기).\n * - live 커넥터 경로: reference-live(어댑터 openLiveFeed→여기).\n * SoT: design/integration/face2-adapters.md §5(seam)·§7(매핑=밖, 검증=커널).\n */\n\n/*\n * **표준 헤더 필드는 한 번만 적는다** — 세 규칙이 이것을 펼쳐 쓴다.\n *\n * ── 왜 뽑아냈나 (2026-09-07) ────────────────────────────────────────────────\n * 이 파일은 규칙 셋을 손으로 나란히 적고 있었고, 그래서 헤더 필드를 더할 때마다 **세 곳을 다 고쳐야**\n * 했다. 그 구조가 같은 결함을 네 번 만들었다.\n *\n * ```\n * quantityList 낱개 번호 없는 자재의 잔량을 말할 길이 없었다\n * ilmd 포천 재고 1,704행 전부에 소비기한이 있는데 상태에는 0건\n * errorDeclaration 정정이 또 하나의 사실로 쌓였다\n * bizTransactionList 저널에 칸·인덱스·질의가 다 있는데 실 시스템 연결에서 0건\n * ```\n *\n * 네 번째는 계약을 열고도 **이 파일이 그 스펙을 안 써서** 여전히 사라지고 있었다. 인티그레이션 레인이\n * 규칙 그대로 `mapRecord` 를 돌려서 찾았다 — 읽어서 맞다고 하지 않고 돌려 본 것이 잡았다.\n *\n * 이제 헤더 필드를 더하는 것은 **이 상수 한 줄**이고, 세 갈래에 동시에 닿는다.\n *\n * `action` 은 여기 없다 — `TransformationEvent` 에는 그 칸이 없다(무엇이 무엇으로 되었나에는 방향이\n * 하나뿐이다). 그것만 규칙마다 적는다.\n */\nconst TWIN_INGEST_HEADER = {\n bizStep: '$.bizStep',\n disposition: '$.disposition',\n /*\n * **정정 선언** — 「이 사건은 없던 일이 되었다」(커널 0.7.65 에서 이 자리가 열렸다).\n *\n * 세 값이 함께여야 뜻이 있어 묶음으로 온다. 커널이 선언 시각을 요구하고, 없으면 사건 시각으로\n * 메우지 않고 이유와 함께 돌려준다 — 「언제 정정했나」와 「언제 일어났나」가 같아지면 둘을 다시\n * 갈라낼 수 없다.\n *\n * 상태는 이것으로 흔들리지 않는다. 커널은 정정을 목록에 적고 재고를 그대로 둔다 — 무엇을 어떻게\n * 되돌릴지는 도메인 판단이고, 참 재고는 그 뒤의 잔량 관측이 다시 말해 준다.\n */\n errorDeclaration: {\n declarationTime: '$.errorDeclaration.declarationTime',\n reason: '$.errorDeclaration.reason',\n correctiveEventIDs: '$.errorDeclaration.correctiveEventIDs'\n },\n /*\n * **오더 참조** — 「이 물건은 어느 오더 것인가」(표준 §7.3.5.4, 계약 0.9.11 에서 이 자리가 열렸다).\n *\n * 받는 쪽은 처음부터 다 서 있었다: 저널에 `biz_transaction` 칸과 인덱스(`ix_twin_event_9`), 이력추적\n * 질의가 그 칸으로 거르고, 커널이 네 자리에서 그것을 채운다. 그래서 커널 안쪽에서 나는 사실만 그\n * 칸을 갖고 실 시스템이 먹이는 트윈은 조용히 비었다 — 시뮬 2,492건 대 `mes-line-a` 0건.\n */\n bizTransactionList: '$.bizTransactionList',\n readPoint: '$.readPoint',\n bizLocation: '$.bizLocation'\n} as const\n\n/** 고정 정규 매핑(방언 아님 — 필드명 자체가 정규 계약). `sourceType` 으로 어느 품목 사실인지 고른다. */\nconst TWIN_INGEST_RULES: AdapterRule[] = [\n {\n sourceType: 'twin',\n mapping: {\n type: 'ObjectEvent',\n ...TWIN_INGEST_HEADER,\n action: '$.action',\n epc: '$.epc',\n /* 비직렬 수량 — 개체 없이 「이 자리에 이 품목이 얼마 남았다」만 오는 관측이 있다. */\n quantityList: '$.quantityList',\n /*\n * **개체·로트의 마스터데이터**(로트 번호·소비기한 등) — 커널 0.7.58 에서 이 자리가 열렸다.\n *\n * 실측(포천): 원본 재고 1,704행 **전부**에 소비기한이 있는데 투영된 상태에는 **0건**이었다. 지난\n * 재고 950건·59,785kg 이 어디에도 나타나지 않았고, 빈 화면이 「이상 없음」으로 읽혔다.\n *\n * ★ **표준이 `action: 'ADD'` 일 때만 허용한다**(또는 변환). 마스터데이터는 로트가 **생길 때**\n * 말하는 사실이고, 주기 관측마다 실으면 커널이 거부한다 — 알리지 않고 통과시키지 않는 것이 옳다\n * (커넥터가 첫 목격에 `ADD`, 그 뒤로 `OBSERVE` 를 내야 한다).\n */\n ilmd: '$.ilmd',\n /*\n * **정정 선언** — 「이 사건은 없던 일이 되었다」(커널 0.7.65 에서 이 자리가 열렸다).\n *\n * 세 값이 함께여야 뜻이 있어 묶음으로 온다. 커널이 선언 시각을 요구하고, 없으면 사건 시각으로\n * 메우지 않고 이유와 함께 돌려준다 — 「언제 정정했나」와 「언제 일어났나」가 같아지면 둘을 다시\n * 갈라낼 수 없다.\n *\n * 상태는 이것으로 흔들리지 않는다. 커널은 정정을 목록에 적고 재고를 그대로 둔다 — 무엇을 어떻게\n * 되돌릴지는 도메인 판단이고, 참 재고는 그 뒤의 잔량 관측이 다시 말해 준다.\n */\n }\n },\n {\n /* **변환의 사실** — 무엇으로 무엇이 되었나. 이름은 표준의 것을 그대로 쓴다(갈래 판정이 그 이름을 본다). */\n sourceType: 'twin-transformation',\n mapping: {\n type: 'TransformationEvent',\n ...TWIN_INGEST_HEADER,\n inputEPCList: '$.inputEPCList',\n inputQuantityList: '$.inputQuantityList',\n outputEPCList: '$.outputEPCList',\n outputQuantityList: '$.outputQuantityList',\n transformationID: '$.transformationID',\n }\n },\n {\n /*\n * **담김의 사실** — 무엇이 무엇에 실렸나(팔레트·상자). 오래 비어 있던 자리다: 관측 리듀서는 이 사실을\n * 받으면 자식 물품에 `parent` 를 붙이는데, 문이 `ObjectEvent` 하나만 알아서 원본은 적재를 말할 길이\n * 없었다(시뮬 원본을 붙여 돌려 보고서야 로그로 드러났다).\n */\n sourceType: 'twin-aggregation',\n mapping: {\n type: 'AggregationEvent',\n ...TWIN_INGEST_HEADER,\n action: '$.action',\n parentID: '$.parentID',\n childEPCs: '$.childEPCs',\n /* 낱개 식별자 없이 **수량으로** 담긴 자식(「이 품번 40개」) — 입고·포장이 이 모양으로 온다. */\n childQuantityList: '$.childQuantityList',\n }\n }\n]\n\n/*\n * 규칙을 시험이 돌릴 수 있게 낸다 — **읽어서 맞다고 하지 않기 위해.**\n *\n * 오늘 이 자리가 정확히 그것으로 새고 있었다: 계약을 열고 계약 시험을 초록으로 만든 뒤에도, 이 파일이\n * 그 스펙을 안 써서 값이 계속 사라졌다. 인티그레이션 레인이 이 규칙 그대로 `mapRecord` 를 돌려 봐서\n * 찾았다 — 규칙이 파일 안에 갇혀 있으면 그것을 돌려 볼 방법이 없다.\n *\n * 복사본을 낸다. 부르는 쪽이 고쳐도 유입 경로가 바뀌지 않아야 한다.\n */\nexport function canonicalRulesForTest(): AdapterRule[] {\n return TWIN_INGEST_RULES.map(r => ({ ...r, mapping: { ...r.mapping } }))\n}\n\n/*\n * 정규 레코드 셋(`CanonicalRecord` · `CanonicalTransformationRecord` · `CanonicalAggregationRecord`)은\n * **계약으로 옮겼다**(2026-08-30, `@operato/ops-contract` 0.2.0).\n *\n * 여기 있던 이유는 소비 방식 때문이었다 — EPCIS 는 커널의 범용 어댑터를 지나서 타입을 요구하지 않았다.\n * 그런데 모양 자체는 EPCIS 2.0 의 필드 이름이고 호스트의 것이 아니다. 이제 MES 가 이 모양 그대로\n * 보낸다.\n *\n * **매핑 룰(`TWIN_INGEST_RULES`)은 여기 남는다** — 이 모양을 EPCIS 사건으로 옮기는 정책이지 모양이 아니다.\n */\n\n/** 정규 레코드[] → 검증된 CanonicalEnvelope[] (+ 거부분). 순수 함수. 단일→객체도 배열로 정규화. */\nexport function ingestCanonicalRecords(\n records:\n | CanonicalRecord\n | CanonicalAggregationRecord\n | CanonicalTransformationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n /* 다섯째 어휘 — 사건이 아닌 것(변하지 않는 속성). 저널로 가지 않는다(§`CanonicalIngestResult`). */\n | MasterDataRecord\n | (\n | CanonicalRecord\n | CanonicalAggregationRecord\n | CanonicalTransformationRecord\n | EnergyRecord\n | EnergyEquipmentRecord\n | OperationalRecord\n | MasterDataRecord\n )[]\n | undefined\n | null,\n tenantId: string,\n defaultEventTime: string,\n /**\n * **이 사실들이 어느 트윈의 것인가** — 마감된 구간 사실의 이름에 쓰인다(커널 §`SubjectBasis`).\n *\n * 커널은 도메인으로 서므로 자기가 어느 트윈인지 모른다. 그런데 설비 번호·계량 지점 번호는 트윈\n * 안에서만 통하는 이름표라, 한 도메인에 현장이 둘이면 서로 다른 설비의 같은 날이 한 사실이 된다.\n * 실측으로 그 일이 났다 — 두 발전소의 `002` 가 겹쳤다.\n *\n * 주지 않으면 이름표를 그대로 쓰고 `twin-local` 로 표시된다 — 겹칠 수 있다는 뜻이다.\n */\n scope?: { scopeId?: string; identityOf?: (kind: 'equipment' | 'meter', localId: string) => string | undefined }\n): CanonicalIngestResult {\n const arr = Array.isArray(records) ? records : records ? [records] : []\n /*\n * ── **다섯째 어휘는 사건이 아니다** (2026-08-28) ────────────────────────────\n *\n * 마스터데이터 — 변하지 않는 속성(유통기한·로트번호·자리의 성질). 표준은 이것을 사건이 아니라\n * **어휘 요소의 속성**으로 담고, 커널에 그 자리가 없어서 지금까지 사건에 실려 왔다.\n *\n * 그 결과가 유입에서 이렇게 났다. 전량을 읽는 원본이 유통기한을 실으려고 **들어오지 않은 것을\n * `ADD`** 라고 말했고, `ilmd` 가 사건 단위라 자리별로 묶을 수도 없어 낱개로 나갔다 — 재기동 한 번에\n * 2,665건이다.\n *\n * 그래서 여기서 갈라 낸다. 이것은 봉투가 되지 않고 저널에도 적히지 않는다(시각축이 없는 값이다).\n * 갈래 판정은 커널의 것을 부른다 — 다른 넷과 같은 규율이다.\n *\n * 설계: `operato-twin/design/plans/master-data.md`\n */\n const masterCandidates = arr.filter(isMasterDataRecord)\n const master = masterCandidates.length ? ingestMasterData(masterCandidates) : { accepted: [], rejected: [] }\n /*\n * **어휘가 넷이다** — 물류(EPCIS)·에너지 계량·설비 에너지·운영 사실. 예전에는 이 함수가 무조건\n * `ObjectEvent` 를 만들었고, 그래서 계측을 이 길로 넣으면 `epc` 가 없어 거부되거나(그나마 나은 쪽)\n * 엉뚱한 물품 관측이 됐다.\n *\n * 물류 어휘 안에는 모양이 둘이다(개체 관측 `epc` / 담김 `parentID`+`childEPCs`).\n *\n * 판정은 커널이 한 곳에서 한다(`isEnergyRecord`·`isOperationalRecord`·`isAggregationRecord`) — 여기서 다시 짐작하면\n * 소비처마다 답이 달라진다. 봉투는 같은 것을 쓰므로 아래 소비처(미러·저널·브로드캐스팅)는 이 갈림을 알\n * 필요가 없다.\n */\n /*\n * 여섯째·일곱째 어휘 — **마감된 사용 구간**과 **청구서**. 커널 0.7.72 가 이 문들을 열었다.\n *\n * ── ★ 사용 구간을 계량보다 **먼저** 가른다 ─────────────────────────────────\n * `isEnergyRecord` 는 `meterId` 가 있으면 참이므로 사용 구간에도 참이다. 순서를 정하지 않으면\n * 시간별 사용량이 계량 표본으로 들어가고, 그 문은 `kWh` 를 **적산 레지스터**로 읽는다. 그러면\n * 커널이 구간 전력량을 차분으로 구하는데 우리가 넣은 값이 이미 구간의 양이다 — 차분의 차분이 되어\n * 값이 조용히 틀린다. 거부되지도 않는다.\n */\n const usagePeriods = arr.filter(isEnergyUsagePeriodRecord) as unknown as EnergyUsagePeriodRecord[]\n const bills = arr.filter(isEnergyBillRecord) as unknown as EnergyBillRecord[]\n /*\n * 여덟째 어휘 — **그 주기에 적용되는 요금 기준**. 커널 0.7.76 이 이 문을 열었다.\n *\n * ★ 청구서와 겹친다. 청구서가 `billingDemandKW` 를 실을 수 있고, 그러면 두 판정이 다 참이다.\n * 그때 요금 기준 문으로 보내면 「금액은 이 문이 받지 않는다」로 거부된다 — 정산이 통째로\n * 사라진다. 그래서 청구서인 것을 뺀다. 커널에서 배타로 만들어 달라고 올렸다.\n */\n const tariffBases = arr.filter(r => isEnergyTariffBasisRecord(r) && !isEnergyBillRecord(r)) as unknown as EnergyTariffBasisRecord[]\n /*\n * 아홉째 어휘 — **발전 단가**. 낸 것에 매겨지는 1kWh 당 값이고 날마다 바뀐다(커널 0.7.79).\n *\n * 다른 문과 겹치지 않는다: 계량 지점이나 설비 id 가 있으면 이 문이 아니고(커널이 배타로 막는다),\n * 요금적용전력·기본요금 단가가 없으므로 요금 기준도 아니다.\n */\n const generationPrices = arr.filter(isEnergyGenerationPriceRecord) as unknown as EnergyGenerationPriceRecord[]\n /* 마감된 발전 기간 — 지난 기록 채우기가 쓴다. 적산의 문과 배타다(구간이 있으면 이쪽). */\n const generationPeriods = arr.filter(isEnergyGenerationPeriodRecord) as unknown as EnergyGenerationPeriodRecord[]\n const energy = arr.filter(r => isEnergyRecord(r) && !isEnergyUsagePeriodRecord(r)) as EnergyRecord[]\n /* 세 번째 어휘 — 설비가 낸 자기 에너지 상태(발전·저장·감축 여지·개폐 위치). 계량과 갈라 두는\n 이유는 커널에 적혀 있다: 계량은 구간에 누적되고, 이것들은 그 설비의 지금이다. */\n /*\n * 다섯 번째 어휘 — **발전 적산**(설비가 지금까지 만든 양). 커널 0.7.69 가 이 문을 열었다.\n *\n * ── ★ 설비 상태보다 **먼저** 가른다 ──────────────────────────────────────────\n * 두 판정이 겹친다. `isEnergyEquipmentRecord` 는 `equipmentId` 가 있고 물류·계량 어휘가 아니면\n * 참이므로 발전 적산도 함께 참이다. 그래서 순서가 갈림을 정한다.\n *\n * 순서를 잘못 두면 조용히 사라지는 것이 아니라 **엉뚱한 이유로 거부된다**: 설비 상태 문은 적산을\n * 값으로 세지 않아 「바꿀 상태가 하나도 없다」로 돌려보낸다. 커넥터는 보냈다고 믿고, 화면에는\n * 발전량이 없다.\n */\n const energyGeneration = arr.filter(isEnergyGenerationRecord) as unknown as EnergyGenerationRecord[]\n const energyEquipment = arr.filter(r => isEnergyEquipmentRecord(r) && !isEnergyGenerationRecord(r)) as unknown as EnergyEquipmentRecord[]\n /*\n * 네 번째 어휘 — **운영 사실**(작업·설비·사람·자산·오더·품질). 관측 리듀서는 이것을 오래전부터\n * 계산했는데 **들어올 문이 없었다**: 원본이 「이 작업이 끝났다」를 말할 길이 없어 시뮬 커널만 그것을\n * 아는 상태가 남았다(같은 화면이 두 구동에서 다른 것을 말한다).\n */\n const operational = arr.filter(isOperationalRecord) as OperationalRecord[]\n /*\n * ── 이 목록은 **어휘를 더할 때마다 함께 늘어야 한다** (2026-08-30) ─────────\n *\n * EPCIS 갈래는 「나머지 전부」다. 새 어휘를 빼 주지 않으면 그 레코드가 제 문으로 가면서\n * **EPCIS 문으로도 들어가** 거부된다. 받아들여지기도 하고 거부되기도 하는 상태가 되어,\n * 유입 장부의 거부 수가 늘고 사람이 그것을 진짜 결함으로 읽는다.\n *\n * 실제로 발전 단가를 더할 때 그렇게 됐다. 목록을 손으로 지키는 대신 시험이 지킨다\n * (§`canonical-ingest-vocabularies` — 어휘마다 한 문으로만 가는지).\n */\n const epcis = arr.filter(\n r =>\n !isEnergyRecord(r) &&\n !isEnergyEquipmentRecord(r) &&\n !isEnergyGenerationRecord(r) &&\n !isEnergyUsagePeriodRecord(r) &&\n !isEnergyBillRecord(r) &&\n !isEnergyTariffBasisRecord(r) &&\n !isEnergyGenerationPriceRecord(r) &&\n !isEnergyGenerationPeriodRecord(r) &&\n !isOperationalRecord(r) &&\n !isMasterDataRecord(r)\n ) as (CanonicalRecord | CanonicalAggregationRecord | CanonicalTransformationRecord)[]\n\n /* 품목 어휘 안에서도 갈림이 있다(개체 관측 / 담김) — 그 판정도 커널의 것을 부른다. */\n const epcisResult = epcis.length\n ? ingest(\n epcis.map(r => ({\n ...r,\n /*\n * ── **시각의 이름을 둘 흡수한다** (2026-08-24 실측) ────────────────────\n * `eventTimePath` 는 **한 이름**만 받는다. 호스트가 `'eventTime'` 으로 못박아 두었는데, 뒤에\n * 생긴 커넥터는 `at` 을 싣는다(에너지 채널이 이미 `at` 을 쓰고 있어 그 어휘를 따랐다).\n * 그래서 그 커넥터의 사건은 **전부 폴링 순간**으로 찍혔다 — 저널 18,150건이 그랬다.\n *\n * 그 결과 둘이 함께 망가졌다: 재고 관측이 트윈의 시계를 밀지 못해 시계가 `task.status` 의\n * 최댓값(2026-04-15)에 **멈춰 있었고**, 이동 사건에서 「언제」가 사라져 순서만 남았다.\n *\n * 이름을 하나로 강요하지 않고 흡수한다 — `orderId`·`resourceRef`·`equipmentId` 를 함께 보는\n * 것과 같은 규율이다(§`twin-event-keys`). 세대가 섞이는 것은 이 이음새의 성질이다.\n *\n * **없으면 만들지 않는다**: 절대값 스냅샷 관측은 원본의 갱신 시각이 아니라 **폴링 순간**이\n * 맞는 관측 시각이다(「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다). 값을 말하는\n * 레코드만 자기 시각을 갖는다.\n */\n /*\n * ── 세 번째로 같은 자리를 밟았다 (2026-09-06) ────────────────────────\n * 여기가 `eventTime ?? at` **두 이름만** 보고 있었다. `nonconformance.disposition` 은\n * `data.decidedAt` 을 싣고(contract 가 required), 그래서 ingest 시각으로 떨어졌다 —\n * backfill 로 5일 지난 사실을 받으니 5일 틀린 값이 저장됐다.\n *\n * 이름을 하나 더 넣는 것으로는 안 끝난다. type 마다의 칸을 한 표에 두고, 못 읽은 것은\n * 세어서 낸다(§`event-time.ts`).\n */\n ...(factEventTime(r) !== undefined ? { eventTime: factEventTime(r) } : {}),\n /* 판정은 커널이 한 곳에서 한다 — 여기서 다시 짐작하면 소비처마다 답이 달라진다. */\n sourceType: isTransformationRecord(r)\n ? 'twin-transformation'\n : isAggregationRecord(r)\n ? 'twin-aggregation'\n : 'twin'\n })),\n TWIN_INGEST_RULES,\n /*\n * **레코드가 시각을 말하면 그것을 쓴다** (2026-08-23).\n *\n * `eventTimePath` 를 주지 않던 동안 이 경로는 **언제나 폴링 시각**을 썼다(`defaultEventTime`).\n * 그래서 커넥터가 이미 싣고 있던 `eventTime`(변환 레코드의 `finishedAt` 등)이 **알리지 않고\n * 무시됐다** — 커넥터를 붙인 쪽은 그것이 먹는다고 믿고 있었고, 어디에도 오류가 나지 않았다.\n *\n * 무엇이 걸렸나: 계보(변환)의 시각이다. 4월에 만든 로트가 8월에 만들어진 것으로 저널에 남았다.\n * 그리고 실측 소요를 재는 폴드가 봉투 시각을 쓰므로(§`kpi-fold`: `workMs = 완료 − 첫 착수`),\n * 폴링 간격을 작업시간으로 배울 수 있었다 — 못 배우는 것보다 나쁘다.\n *\n * 레코드에 `eventTime` 이 없으면 커널이 알리지 않고 기본값으로 떨어진다(§`resolve` — 없는 값은\n * 오류가 아니다). 그래서 이 한 줄이 기존 경로를 막지 않는다: 시각을 말하는 레코드만 사실이 된다.\n */\n { tenantId, defaultEventTime, eventTimePath: 'eventTime' }\n )\n : { accepted: [], rejected: [] }\n const energyResult = energy.length ? ingestEnergyRecords(energy, { tenantId, defaultEventTime, ...scope }) : { accepted: [], rejected: [] }\n const equipmentResult = energyEquipment.length\n ? ingestEnergyEquipmentRecords(energyEquipment, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const usageResult = usagePeriods.length\n ? ingestEnergyUsagePeriodRecords(usagePeriods, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const billResult = bills.length ? ingestEnergyBillRecords(bills, { tenantId, defaultEventTime, ...scope }) : { accepted: [], rejected: [] }\n const tariffBasisResult = tariffBases.length\n ? ingestEnergyTariffBasisRecords(tariffBases, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationPeriodResult = generationPeriods.length\n ? ingestEnergyGenerationPeriodRecords(generationPeriods, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationPriceResult = generationPrices.length\n ? ingestEnergyGenerationPriceRecords(generationPrices, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n const generationResult = energyGeneration.length\n ? ingestEnergyGenerationRecords(energyGeneration, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n /*\n * ── fact type 마다 시각 칸 이름이 다르다 (2026-09-06) ────────────────────────\n * 운영 레코드의 봉투 시각은 `at` 이다. 그런데 `nonconformance.disposition` 은 자기 시각을\n * **payload 의 `decidedAt`** 에 싣는다(contract 가 그 자리에 required 로 선언했다). 그래서 `at` 이\n * 없고 `defaultEventTime`(= ingest 시각)으로 떨어졌다.\n *\n * ```\n * [측정함 2026-09-06, mes-line-a backfill 뒤]\n * event_time 2026-09-05 23:50:51 data.decidedAt 2026-09-01T02:50:51.832Z 5일 차이\n * ```\n *\n * push 경로에서는 몇 초 차이라 아무도 못 알아챈다. backfill 이 그 간격을 벌려서 드러냈다 —\n * backfill 이 만든 결함이 아니라 원래 있던 것이 보인 것이다.\n *\n * 여기서 `at` 을 채워 넣는다. 더 깊은 고침은 contract 쪽이지만(선언과 봉투가 다른 이름을 쓰는\n * 비대칭), 그것은 배포를 기다려야 하고 그동안 값이 계속 틀리게 저장된다.\n */\n const operationalDated = operational.map(r => {\n const at = factEventTime(r)\n return at !== undefined && (r as any).at === undefined ? { ...(r as any), at } : r\n })\n const operationalResult = operationalDated.length\n ? ingestOperationalRecords(operationalDated, { tenantId, defaultEventTime, ...scope })\n : { accepted: [], rejected: [] }\n\n /* 거부분을 합쳐 돌려준다 — 어느 어휘에서 거부됐는지는 이유 문구가 말한다. */\n return {\n accepted: [\n ...epcisResult.accepted,\n ...energyResult.accepted,\n ...equipmentResult.accepted,\n ...generationResult.accepted,\n ...usageResult.accepted,\n ...generationPriceResult.accepted,\n ...generationPeriodResult.accepted,\n ...billResult.accepted,\n ...tariffBasisResult.accepted,\n ...operationalResult.accepted\n ],\n rejected: [\n ...epcisResult.rejected,\n ...energyResult.rejected,\n ...equipmentResult.rejected,\n ...generationResult.rejected,\n ...usageResult.rejected,\n ...billResult.rejected,\n ...tariffBasisResult.rejected,\n ...generationPriceResult.rejected,\n ...generationPeriodResult.rejected,\n ...operationalResult.rejected,\n ...master.rejected\n ],\n /* 사건이 아닌 것은 따로 낸다 — 부르는 쪽이 상태 세우는 문으로 보낸다(저널로 가지 않게). */\n masterData: master.accepted,\n /*\n * **시각을 못 읽어 ingest 시각으로 떨어진 것** — type 별 수.\n *\n * 오류가 아니다. 절대값 snapshot 은 polling 순간이 맞는 시각이라 여기 세어지는 것이 정상이다.\n * 다만 그 수가 어디에도 안 남으면 `event_time == created_at` 이 유일한 단서가 되고, 그것은\n * 우연히 같은 것과 구별되지 않는다.\n */\n undated: undatedByKind([...epcis, ...operational])\n }\n}\n\n/**\n * 유입 결과 — 사건(봉투)과 **사건이 아닌 것**을 갈라 낸다.\n *\n * 마스터데이터를 `accepted` 에 섞지 않는 이유: 부르는 쪽이 그것을 저널에 적게 되고, 시각축이 없는 값이\n * 「그때 일어난 일」로 남는다. 갈래를 타입으로 갈라 두면 그 실수를 할 수 없다.\n */\nexport interface CanonicalIngestResult extends IngestResult {\n masterData: MasterDataRecord[]\n /**\n * 시각을 못 읽어 ingest 시각으로 떨어진 record 를 **type 별로** 센다.\n *\n * 비어 있으면 전부 자기 시각을 말한 것이다. 값이 있어도 오류는 아니다 — 절대값 snapshot 은\n * polling 순간이 맞는 시각이다. 다만 그 수가 남아야 「5일 틀린 값이 저장됐다」를 나중에 찾을 수\n * 있다(2026-09-06 에 그것을 backfill 로 찾았다).\n */\n undated: Record<string, number>\n}\n"]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 운영 fact 종류가 자기 시각을 싣는 칸 — 봉투의 `at` 이 없을 때 읽는다.
3
+ *
4
+ * 새 종류가 자기 시각을 payload 에 실으면 여기 한 줄을 더한다.
5
+ */
6
+ export declare const KIND_TIME_FIELD: Record<string, string>;
7
+ /**
8
+ * 이 레코드가 말하는 시각. **못 읽으면 `undefined`** — 여기서 지금 시각으로 채우지 않는다.
9
+ *
10
+ * 보는 순서는 공통 이름이 먼저다. connector 가 봉투 층에서 시각을 실어 주면 그것이 그 레코드에 대한
11
+ * 가장 바깥의 판단이고, 종류마다의 칸은 그것이 없을 때 읽는다.
12
+ */
13
+ export declare function factEventTime(record: unknown): string | undefined;
14
+ /**
15
+ * 시각을 못 읽은 레코드를 종류별로 센다 — **조용히 ingest 시각으로 떨어진 것들.**
16
+ *
17
+ * 오류가 아니다. 절대값 snapshot 관측은 원본의 갱신 시각이 아니라 polling 순간이 맞는 시각이라
18
+ * (「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다) 여기 세어지는 것이 정상이다.
19
+ *
20
+ * 다만 그 수가 어디에도 안 남으면 `event_time == created_at` 이 유일한 단서가 되고, 그것은 우연히
21
+ * 같은 것과 구별되지 않는다.
22
+ */
23
+ export declare function undatedByKind(records: readonly unknown[]): Record<string, number>;
24
+ /** 표에 있는 종류인데 시각을 못 읽었다 — 그것은 실제로 결함이다. */
25
+ export declare function shouldHaveTime(kind: string): boolean;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KIND_TIME_FIELD = void 0;
4
+ exports.factEventTime = factEventTime;
5
+ exports.undatedByKind = undatedByKind;
6
+ exports.shouldHaveTime = shouldHaveTime;
7
+ /*
8
+ * **레코드가 말하는 「언제 일어났나」를 읽는다** — fact 종류마다 칸 이름이 다르다.
9
+ *
10
+ * ── 같은 defect 가 세 번째다 ────────────────────────────────────────────────
11
+ * `canonical-ingest.ts` 가 `eventTime ?? at` **두 이름만** 보고 있었다. 그 주석에 이미 두 번의 사고가
12
+ * 적혀 있다 — 처음엔 `eventTime` 하나만 봐서 energy connector 의 사실 18,150건이 polling 시각으로
13
+ * 찍혔고, 그 다음에 `at` 을 더했다.
14
+ *
15
+ * 세 번째는 `nonconformance.disposition` 이었다.
16
+ *
17
+ * ```
18
+ * [측정함 2026-09-06, mes-line-a backfill 뒤]
19
+ * event_time 2026-09-05 23:50:51 payload decidedAt 2026-09-01T02:50:51.832Z 5일 차이
20
+ * ```
21
+ *
22
+ * ── root cause: 선언과 봉투가 다른 이름을 쓴다 ──────────────────────────────
23
+ * 운영 레코드의 봉투 시각은 `at` 이다(`OperationalRecordEnvelopeFields`). 그런데 처분 fact 는 자기
24
+ * 시각을 **payload 의 `decidedAt`** 에 싣는다 — contract 가 그 자리에 required 로 선언했고 plant 도
25
+ * 맞게 보낸다. 읽는 쪽에 그 이름이 없어서 `defaultEventTime`(= ingest 시각)으로 떨어졌다.
26
+ *
27
+ * ── push 경로에서는 안 보인다 ───────────────────────────────────────────────
28
+ * 사실이 나자마자 보내면 ingest 시각과 event 시각이 몇 초 차이다. backfill 이 그 간격을 벌려서
29
+ * 드러냈다 — **backfill 이 만든 defect 가 아니라 원래 있던 것이 보인 것**이다.
30
+ *
31
+ * ── 이름을 늘리는 것으로는 안 끝난다 ────────────────────────────────────────
32
+ * 다음 fact 종류가 또 다른 이름을 쓰면 같은 자리를 밟는다. 그래서 두 가지를 함께 한다.
33
+ *
34
+ * ```
35
+ * 1 종류마다의 칸 이름을 한 표에 둔다 새 종류를 더할 때 여기가 눈에 띈다
36
+ * 2 못 읽으면 그 수를 낸다 조용히 ingest 시각으로 떨어지지 않는다
37
+ * ```
38
+ *
39
+ * 종류 판정은 **contract 의 것을 부른다**(`operationalKindOf`). 여기서 다시 짐작하면 두 곳이 다른
40
+ * 답을 내고, 그때 어느 쪽이 맞는지 아무도 모른다.
41
+ */
42
+ const ops_contract_1 = require("@operato/ops-contract");
43
+ /**
44
+ * 운영 fact 종류가 자기 시각을 싣는 칸 — 봉투의 `at` 이 없을 때 읽는다.
45
+ *
46
+ * 새 종류가 자기 시각을 payload 에 실으면 여기 한 줄을 더한다.
47
+ */
48
+ exports.KIND_TIME_FIELD = {
49
+ /* `DispositionFact.decidedAt` — contract 가 required 로 선언했다. */
50
+ disposition: 'decidedAt'
51
+ };
52
+ /*
53
+ * `equipment-period` 는 일부러 안 넣는다. 그 fact 의 봉투 시각은 **구간의 끝**이고(그때 성립한다),
54
+ * `recordTime` 은 「그 구간을 언제 적었나」다. 둘이 갈려 있어야 늦게 적은 구간이 늦게 일어난 것으로
55
+ * 읽히지 않는다 — contract 가 그 판단을 이미 적어 두었다.
56
+ */
57
+ function iso(v) {
58
+ return typeof v === 'string' && v.trim() ? v : undefined;
59
+ }
60
+ /**
61
+ * 이 레코드가 말하는 시각. **못 읽으면 `undefined`** — 여기서 지금 시각으로 채우지 않는다.
62
+ *
63
+ * 보는 순서는 공통 이름이 먼저다. connector 가 봉투 층에서 시각을 실어 주면 그것이 그 레코드에 대한
64
+ * 가장 바깥의 판단이고, 종류마다의 칸은 그것이 없을 때 읽는다.
65
+ */
66
+ function factEventTime(record) {
67
+ const r = record;
68
+ const common = iso(r?.eventTime) ?? iso(r?.at);
69
+ if (common)
70
+ return common;
71
+ const kind = (0, ops_contract_1.operationalKindOf)(record);
72
+ const named = kind ? exports.KIND_TIME_FIELD[kind] : undefined;
73
+ return named ? iso(r?.[named]) : undefined;
74
+ }
75
+ /**
76
+ * 시각을 못 읽은 레코드를 종류별로 센다 — **조용히 ingest 시각으로 떨어진 것들.**
77
+ *
78
+ * 오류가 아니다. 절대값 snapshot 관측은 원본의 갱신 시각이 아니라 polling 순간이 맞는 시각이라
79
+ * (「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다) 여기 세어지는 것이 정상이다.
80
+ *
81
+ * 다만 그 수가 어디에도 안 남으면 `event_time == created_at` 이 유일한 단서가 되고, 그것은 우연히
82
+ * 같은 것과 구별되지 않는다.
83
+ */
84
+ function undatedByKind(records) {
85
+ const out = {};
86
+ for (const r of records) {
87
+ if (factEventTime(r) !== undefined)
88
+ continue;
89
+ const kind = (0, ops_contract_1.operationalKindOf)(r) ?? r?.type ?? '(종류 모름)';
90
+ out[kind] = (out[kind] ?? 0) + 1;
91
+ }
92
+ return out;
93
+ }
94
+ /** 표에 있는 종류인데 시각을 못 읽었다 — 그것은 실제로 결함이다. */
95
+ function shouldHaveTime(kind) {
96
+ return kind in exports.KIND_TIME_FIELD;
97
+ }
98
+ //# sourceMappingURL=event-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-time.js","sourceRoot":"","sources":["../../server/engine/event-time.ts"],"names":[],"mappings":";;;AA+DA,sCAQC;AAWD,sCAQC;AAGD,wCAEC;AA/FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wDAAyD;AAEzD;;;;GAIG;AACU,QAAA,eAAe,GAA2B;IACrD,+DAA+D;IAC/D,WAAW,EAAE,WAAW;CACzB,CAAA;AAED;;;;GAIG;AAEH,SAAS,GAAG,CAAC,CAAU;IACrB,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC1D,CAAC;AAED;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,MAAe;IAC3C,MAAM,CAAC,GAAG,MAAa,CAAA;IACvB,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,EAAE,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAC9C,IAAI,MAAM;QAAE,OAAO,MAAM,CAAA;IAEzB,MAAM,IAAI,GAAG,IAAA,gCAAiB,EAAC,MAAM,CAAC,CAAA;IACtC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,uBAAe,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACtD,OAAO,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC5C,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,aAAa,CAAC,OAA2B;IACvD,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,aAAa,CAAC,CAAC,CAAC,KAAK,SAAS;YAAE,SAAQ;QAC5C,MAAM,IAAI,GAAG,IAAA,gCAAiB,EAAC,CAAC,CAAC,IAAM,CAAS,EAAE,IAAe,IAAI,SAAS,CAAA;QAC9E,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IAClC,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,2CAA2C;AAC3C,SAAgB,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,IAAI,uBAAe,CAAA;AAChC,CAAC","sourcesContent":["/*\n * **레코드가 말하는 「언제 일어났나」를 읽는다** — fact 종류마다 칸 이름이 다르다.\n *\n * ── 같은 defect 가 세 번째다 ────────────────────────────────────────────────\n * `canonical-ingest.ts` 가 `eventTime ?? at` **두 이름만** 보고 있었다. 그 주석에 이미 두 번의 사고가\n * 적혀 있다 — 처음엔 `eventTime` 하나만 봐서 energy connector 의 사실 18,150건이 polling 시각으로\n * 찍혔고, 그 다음에 `at` 을 더했다.\n *\n * 세 번째는 `nonconformance.disposition` 이었다.\n *\n * ```\n * [측정함 2026-09-06, mes-line-a backfill 뒤]\n * event_time 2026-09-05 23:50:51 payload decidedAt 2026-09-01T02:50:51.832Z 5일 차이\n * ```\n *\n * ── root cause: 선언과 봉투가 다른 이름을 쓴다 ──────────────────────────────\n * 운영 레코드의 봉투 시각은 `at` 이다(`OperationalRecordEnvelopeFields`). 그런데 처분 fact 는 자기\n * 시각을 **payload 의 `decidedAt`** 에 싣는다 — contract 가 그 자리에 required 로 선언했고 plant 도\n * 맞게 보낸다. 읽는 쪽에 그 이름이 없어서 `defaultEventTime`(= ingest 시각)으로 떨어졌다.\n *\n * ── push 경로에서는 안 보인다 ───────────────────────────────────────────────\n * 사실이 나자마자 보내면 ingest 시각과 event 시각이 몇 초 차이다. backfill 이 그 간격을 벌려서\n * 드러냈다 — **backfill 이 만든 defect 가 아니라 원래 있던 것이 보인 것**이다.\n *\n * ── 이름을 늘리는 것으로는 안 끝난다 ────────────────────────────────────────\n * 다음 fact 종류가 또 다른 이름을 쓰면 같은 자리를 밟는다. 그래서 두 가지를 함께 한다.\n *\n * ```\n * 1 종류마다의 칸 이름을 한 표에 둔다 새 종류를 더할 때 여기가 눈에 띈다\n * 2 못 읽으면 그 수를 낸다 조용히 ingest 시각으로 떨어지지 않는다\n * ```\n *\n * 종류 판정은 **contract 의 것을 부른다**(`operationalKindOf`). 여기서 다시 짐작하면 두 곳이 다른\n * 답을 내고, 그때 어느 쪽이 맞는지 아무도 모른다.\n */\nimport { operationalKindOf } from '@operato/ops-contract'\n\n/**\n * 운영 fact 종류가 자기 시각을 싣는 칸 — 봉투의 `at` 이 없을 때 읽는다.\n *\n * 새 종류가 자기 시각을 payload 에 실으면 여기 한 줄을 더한다.\n */\nexport const KIND_TIME_FIELD: Record<string, string> = {\n /* `DispositionFact.decidedAt` — contract 가 required 로 선언했다. */\n disposition: 'decidedAt'\n}\n\n/*\n * `equipment-period` 는 일부러 안 넣는다. 그 fact 의 봉투 시각은 **구간의 끝**이고(그때 성립한다),\n * `recordTime` 은 「그 구간을 언제 적었나」다. 둘이 갈려 있어야 늦게 적은 구간이 늦게 일어난 것으로\n * 읽히지 않는다 — contract 가 그 판단을 이미 적어 두었다.\n */\n\nfunction iso(v: unknown): string | undefined {\n return typeof v === 'string' && v.trim() ? v : undefined\n}\n\n/**\n * 이 레코드가 말하는 시각. **못 읽으면 `undefined`** — 여기서 지금 시각으로 채우지 않는다.\n *\n * 보는 순서는 공통 이름이 먼저다. connector 가 봉투 층에서 시각을 실어 주면 그것이 그 레코드에 대한\n * 가장 바깥의 판단이고, 종류마다의 칸은 그것이 없을 때 읽는다.\n */\nexport function factEventTime(record: unknown): string | undefined {\n const r = record as any\n const common = iso(r?.eventTime) ?? iso(r?.at)\n if (common) return common\n\n const kind = operationalKindOf(record)\n const named = kind ? KIND_TIME_FIELD[kind] : undefined\n return named ? iso(r?.[named]) : undefined\n}\n\n/**\n * 시각을 못 읽은 레코드를 종류별로 센다 — **조용히 ingest 시각으로 떨어진 것들.**\n *\n * 오류가 아니다. 절대값 snapshot 관측은 원본의 갱신 시각이 아니라 polling 순간이 맞는 시각이라\n * (「어제 값을 오늘 봤다」는 「어제 일어났다」가 아니다) 여기 세어지는 것이 정상이다.\n *\n * 다만 그 수가 어디에도 안 남으면 `event_time == created_at` 이 유일한 단서가 되고, 그것은 우연히\n * 같은 것과 구별되지 않는다.\n */\nexport function undatedByKind(records: readonly unknown[]): Record<string, number> {\n const out: Record<string, number> = {}\n for (const r of records) {\n if (factEventTime(r) !== undefined) continue\n const kind = operationalKindOf(r) ?? ((r as any)?.type as string) ?? '(종류 모름)'\n out[kind] = (out[kind] ?? 0) + 1\n }\n return out\n}\n\n/** 표에 있는 종류인데 시각을 못 읽었다 — 그것은 실제로 결함이다. */\nexport function shouldHaveTime(kind: string): boolean {\n return kind in KIND_TIME_FIELD\n}\n"]}
@@ -22,3 +22,5 @@ export * from './operation-basis.js';
22
22
  export * from './measured-yield.js';
23
23
  export * from './declared-stimulus.js';
24
24
  export * from './integration-coverage.js';
25
+ export { factIdentity } from './ingest-dedupe.js';
26
+ export { factEventTime, shouldHaveTime, undatedByKind } from './event-time.js';
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.undatedByKind = exports.shouldHaveTime = exports.factEventTime = exports.factIdentity = void 0;
3
4
  const tslib_1 = require("tslib");
4
5
  tslib_1.__exportStar(require("./twin-engine.js"), exports);
5
6
  tslib_1.__exportStar(require("./canonical-ingest.js"), exports);
@@ -32,4 +33,26 @@ tslib_1.__exportStar(require("./operation-basis.js"), exports);
32
33
  tslib_1.__exportStar(require("./measured-yield.js"), exports);
33
34
  tslib_1.__exportStar(require("./declared-stimulus.js"), exports);
34
35
  tslib_1.__exportStar(require("./integration-coverage.js"), exports);
36
+ /*
37
+ * 사실의 정체(identity)와 시각을 판단하는 함수들.
38
+ *
39
+ * 모듈 전체를 export 하지 않고 이 둘만 내보낸다. `FactDeduper` 같은 상태를 가진 클래스는 커널
40
+ * 안에서만 쓰여야 하고, 밖에서 인스턴스를 따로 만들면 중복 판단 기억이 두 벌이 된다.
41
+ *
42
+ * 밖에서 이 둘이 필요한 이유는 하나다 — 저널에 앉은 사실이 제대로 된 정체와 시각을 가졌는지
43
+ * 확인할 때, 유입할 때와 **똑같은 함수로** 판단해야 하기 때문이다. 확인하는 쪽이 규칙을 다시
44
+ * 구현하면 두 규칙이 어긋나는 날 확인이 조용히 통과한다.
45
+ */
46
+ var ingest_dedupe_js_1 = require("./ingest-dedupe.js");
47
+ Object.defineProperty(exports, "factIdentity", { enumerable: true, get: function () { return ingest_dedupe_js_1.factIdentity; } });
48
+ /*
49
+ * `shouldHaveTime` is the part that matters outside: it separates "this record legitimately has no
50
+ * time of its own" (a snapshot observation — the polling moment IS the right time) from "the kind
51
+ * declares a time field and we could not read it", which is a real defect. Anything checking the
52
+ * journal must use this split rather than inventing its own, or it reports healthy rows as broken.
53
+ */
54
+ var event_time_js_1 = require("./event-time.js");
55
+ Object.defineProperty(exports, "factEventTime", { enumerable: true, get: function () { return event_time_js_1.factEventTime; } });
56
+ Object.defineProperty(exports, "shouldHaveTime", { enumerable: true, get: function () { return event_time_js_1.shouldHaveTime; } });
57
+ Object.defineProperty(exports, "undatedByKind", { enumerable: true, get: function () { return event_time_js_1.undatedByKind; } });
35
58
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/engine/index.ts"],"names":[],"mappings":";;;AAAA,2DAAgC;AAChC,gEAAqC;AACrC,wDAA6B;AAC7B,gEAAqC;AACrC,4DAAiC;AACjC,qEAAqE;AACrE,0DAA+B;AAC/B,qGAA0E;AAC1E,0DAA0D;AAC1D,oGAAyE;AACzE,4CAA4C;AAC5C,kGAAuE;AACvE,sCAAsC;AACtC,0DAA+B;AAC/B,wDAA6B;AAC7B,gEAAgE;AAChE,2DAAgC;AAChC,yDAA8B;AAC9B,6DAAkC;AAClC,gEAAqC;AACrC,kEAAuC;AACvC,2DAAgC;AAChC,kEAAuC;AACvC,gDAAgD;AAChD,6DAAkC;AAClC,yDAAyD;AACzD,kEAAuC;AACvC,+DAAoC;AACpC,8DAAmC;AACnC,iEAAsC;AACtC,oEAAyC","sourcesContent":["export * from './twin-engine.js'\nexport * from './canonical-ingest.js'\nexport * from './kpi-fold.js'\nexport * from './property-effects.js'\nexport * from './kpi-baseline.js'\n/* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */\nexport * from './kpi-target.js'\nexport * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js'\n/* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */\nexport * from '@things-factory/headless-twin/dist-shared/entity-delta.js'\n/* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */\nexport * from '@things-factory/headless-twin/dist-shared/twin-level.js'\n/* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */\nexport * from './load-meter.js'\nexport * from './loop-lag.js'\n/* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */\nexport * from './runtime-key.js'\nexport * from './kpi-query.js'\nexport * from './spec-coverage.js'\nexport * from './travel-estimator.js'\nexport * from './measured-estimator.js'\nexport * from './model-basis.js'\nexport * from './live-feed-registry.js'\n/* 동기화 건강 — 「이 트윈이 현장과 맞춰지고 있나」. 판정·추이·사유·표본. */\nexport * from './ingest-health.js'\n/* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */\nexport * from './local-declarations.js'\nexport * from './operation-basis.js'\nexport * from './measured-yield.js'\nexport * from './declared-stimulus.js'\nexport * from './integration-coverage.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/engine/index.ts"],"names":[],"mappings":";;;;AAAA,2DAAgC;AAChC,gEAAqC;AACrC,wDAA6B;AAC7B,gEAAqC;AACrC,4DAAiC;AACjC,qEAAqE;AACrE,0DAA+B;AAC/B,qGAA0E;AAC1E,0DAA0D;AAC1D,oGAAyE;AACzE,4CAA4C;AAC5C,kGAAuE;AACvE,sCAAsC;AACtC,0DAA+B;AAC/B,wDAA6B;AAC7B,gEAAgE;AAChE,2DAAgC;AAChC,yDAA8B;AAC9B,6DAAkC;AAClC,gEAAqC;AACrC,kEAAuC;AACvC,2DAAgC;AAChC,kEAAuC;AACvC,gDAAgD;AAChD,6DAAkC;AAClC,yDAAyD;AACzD,kEAAuC;AACvC,+DAAoC;AACpC,8DAAmC;AACnC,iEAAsC;AACtC,oEAAyC;AAEzC;;;;;;;;;GASG;AACH,uDAAiD;AAAxC,gHAAA,YAAY,OAAA;AAErB;;;;;GAKG;AACH,iDAA8E;AAArE,8GAAA,aAAa,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,8GAAA,aAAa,OAAA","sourcesContent":["export * from './twin-engine.js'\nexport * from './canonical-ingest.js'\nexport * from './kpi-fold.js'\nexport * from './property-effects.js'\nexport * from './kpi-baseline.js'\n/* 지표 정본 표(TWIN_METRIC)·목표 판정 — 소비처(twin-ai 등)가 지표 이름을 다시 열거하지 않게. */\nexport * from './kpi-target.js'\nexport * from '@things-factory/headless-twin/dist-shared/kpi-broadcast.js'\n/* 집약 채널 태그(접두사 + 트윈별 태그 생성) — 소비처가 문자열을 다시 적지 않게 내보낸다. */\nexport * from '@things-factory/headless-twin/dist-shared/entity-delta.js'\n/* 공간 계층의 단계 — 씬 표현·성과 카드·AI 가 같은 목록을 쓴다. */\nexport * from '@things-factory/headless-twin/dist-shared/twin-level.js'\n/* 부하 계기판 — 어떤 작업이 이벤트 루프를 얼마나 쓰는지. */\nexport * from './load-meter.js'\nexport * from './loop-lag.js'\n/* 런타임 레지스트리 키 규약 — 소비처(다른 키 캐시·테스트)가 `:` 를 다시 적지 않게 함께 내보낸다. */\nexport * from './runtime-key.js'\nexport * from './kpi-query.js'\nexport * from './spec-coverage.js'\nexport * from './travel-estimator.js'\nexport * from './measured-estimator.js'\nexport * from './model-basis.js'\nexport * from './live-feed-registry.js'\n/* 동기화 건강 — 「이 트윈이 현장과 맞춰지고 있나」. 판정·추이·사유·표본. */\nexport * from './ingest-health.js'\n/* 로컬 선언(현장·트윈) — 소비처(twin-ai 도구)가 명세·검사 규칙을 다시 적지 않게. */\nexport * from './local-declarations.js'\nexport * from './operation-basis.js'\nexport * from './measured-yield.js'\nexport * from './declared-stimulus.js'\nexport * from './integration-coverage.js'\n\n/*\n * 사실의 정체(identity)와 시각을 판단하는 함수들.\n *\n * 모듈 전체를 export 하지 않고 이 둘만 내보낸다. `FactDeduper` 같은 상태를 가진 클래스는 커널\n * 안에서만 쓰여야 하고, 밖에서 인스턴스를 따로 만들면 중복 판단 기억이 두 벌이 된다.\n *\n * 밖에서 이 둘이 필요한 이유는 하나다 — 저널에 앉은 사실이 제대로 된 정체와 시각을 가졌는지\n * 확인할 때, 유입할 때와 **똑같은 함수로** 판단해야 하기 때문이다. 확인하는 쪽이 규칙을 다시\n * 구현하면 두 규칙이 어긋나는 날 확인이 조용히 통과한다.\n */\nexport { factIdentity } from './ingest-dedupe.js'\n\n/*\n * `shouldHaveTime` is the part that matters outside: it separates \"this record legitimately has no\n * time of its own\" (a snapshot observation — the polling moment IS the right time) from \"the kind\n * declares a time field and we could not read it\", which is a real defect. Anything checking the\n * journal must use this split rather than inventing its own, or it reports healthy rows as broken.\n */\nexport { factEventTime, shouldHaveTime, undatedByKind } from './event-time.js'\n"]}