@things-factory/headless-twin 10.0.8 → 10.0.10

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 (381) hide show
  1. package/dist-server/engine/attention-digest.d.ts +52 -0
  2. package/dist-server/engine/attention-digest.js +76 -0
  3. package/dist-server/engine/attention-digest.js.map +1 -0
  4. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  5. package/dist-server/engine/canonical-ingest.js +24 -2
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.d.ts +33 -0
  8. package/dist-server/engine/command-routing.js +53 -0
  9. package/dist-server/engine/command-routing.js.map +1 -0
  10. package/dist-server/engine/energy-topology.d.ts +86 -0
  11. package/dist-server/engine/energy-topology.js +144 -0
  12. package/dist-server/engine/energy-topology.js.map +1 -0
  13. package/dist-server/engine/index.d.ts +11 -0
  14. package/dist-server/engine/index.js +17 -0
  15. package/dist-server/engine/index.js.map +1 -1
  16. package/dist-server/engine/kpi-baseline.d.ts +78 -0
  17. package/dist-server/engine/kpi-baseline.js +123 -0
  18. package/dist-server/engine/kpi-baseline.js.map +1 -0
  19. package/dist-server/engine/kpi-fold.d.ts +137 -1
  20. package/dist-server/engine/kpi-fold.js +225 -0
  21. package/dist-server/engine/kpi-fold.js.map +1 -1
  22. package/dist-server/engine/kpi-query.d.ts +32 -1
  23. package/dist-server/engine/kpi-query.js +262 -20
  24. package/dist-server/engine/kpi-query.js.map +1 -1
  25. package/dist-server/engine/kpi-target.d.ts +17 -0
  26. package/dist-server/engine/kpi-target.js +24 -2
  27. package/dist-server/engine/kpi-target.js.map +1 -1
  28. package/dist-server/engine/live-attentions.d.ts +1 -0
  29. package/dist-server/engine/live-attentions.js +7 -1
  30. package/dist-server/engine/live-attentions.js.map +1 -1
  31. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  32. package/dist-server/engine/live-feed-registry.js +51 -0
  33. package/dist-server/engine/live-feed-registry.js.map +1 -0
  34. package/dist-server/engine/load-meter.d.ts +181 -0
  35. package/dist-server/engine/load-meter.js +267 -0
  36. package/dist-server/engine/load-meter.js.map +1 -0
  37. package/dist-server/engine/local-declarations.d.ts +262 -0
  38. package/dist-server/engine/local-declarations.js +528 -0
  39. package/dist-server/engine/local-declarations.js.map +1 -0
  40. package/dist-server/engine/model-basis.d.ts +39 -6
  41. package/dist-server/engine/model-basis.js +69 -9
  42. package/dist-server/engine/model-basis.js.map +1 -1
  43. package/dist-server/engine/model-vocabulary.d.ts +17 -0
  44. package/dist-server/engine/model-vocabulary.js +81 -0
  45. package/dist-server/engine/model-vocabulary.js.map +1 -0
  46. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  47. package/dist-server/engine/oee-accumulator.js +26 -1
  48. package/dist-server/engine/oee-accumulator.js.map +1 -1
  49. package/dist-server/engine/operation-basis.d.ts +32 -0
  50. package/dist-server/engine/operation-basis.js +87 -0
  51. package/dist-server/engine/operation-basis.js.map +1 -0
  52. package/dist-server/engine/property-effects.d.ts +30 -0
  53. package/dist-server/engine/property-effects.js +192 -0
  54. package/dist-server/engine/property-effects.js.map +1 -0
  55. package/dist-server/engine/runtime-key.d.ts +15 -0
  56. package/dist-server/engine/runtime-key.js +64 -0
  57. package/dist-server/engine/runtime-key.js.map +1 -0
  58. package/dist-server/engine/spec-coverage.js +3 -3
  59. package/dist-server/engine/spec-coverage.js.map +1 -1
  60. package/dist-server/engine/state-axes.d.ts +21 -0
  61. package/dist-server/engine/state-axes.js +59 -0
  62. package/dist-server/engine/state-axes.js.map +1 -0
  63. package/dist-server/engine/structure-diff.js +1 -1
  64. package/dist-server/engine/structure-diff.js.map +1 -1
  65. package/dist-server/engine/travel-estimator.d.ts +9 -2
  66. package/dist-server/engine/travel-estimator.js +11 -6
  67. package/dist-server/engine/travel-estimator.js.map +1 -1
  68. package/dist-server/engine/twin-engine.d.ts +418 -57
  69. package/dist-server/engine/twin-engine.js +1599 -306
  70. package/dist-server/engine/twin-engine.js.map +1 -1
  71. package/dist-server/engine/warm-start.d.ts +93 -13
  72. package/dist-server/engine/warm-start.js +114 -14
  73. package/dist-server/engine/warm-start.js.map +1 -1
  74. package/dist-server/index.d.ts +1 -0
  75. package/dist-server/index.js +17 -11
  76. package/dist-server/index.js.map +1 -1
  77. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  78. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  79. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  80. package/dist-server/migrations/index.d.ts +2 -0
  81. package/dist-server/migrations/index.js +10 -0
  82. package/dist-server/migrations/index.js.map +1 -0
  83. package/dist-server/service/index.d.ts +3 -2
  84. package/dist-server/service/index.js +25 -17
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/discovery-result.d.ts +34 -0
  87. package/dist-server/service/reference/discovery-result.js +84 -0
  88. package/dist-server/service/reference/discovery-result.js.map +1 -0
  89. package/dist-server/service/reference/ingest-space.d.ts +53 -0
  90. package/dist-server/service/reference/ingest-space.js +79 -0
  91. package/dist-server/service/reference/ingest-space.js.map +1 -0
  92. package/dist-server/service/reference/knob-defaults.d.ts +20 -0
  93. package/dist-server/service/reference/knob-defaults.js +59 -0
  94. package/dist-server/service/reference/knob-defaults.js.map +1 -0
  95. package/dist-server/service/reference/reference-live.d.ts +23 -0
  96. package/dist-server/service/reference/reference-live.js +123 -6
  97. package/dist-server/service/reference/reference-live.js.map +1 -1
  98. package/dist-server/service/reference/reference-master.d.ts +227 -11
  99. package/dist-server/service/reference/reference-master.js +206 -25
  100. package/dist-server/service/reference/reference-master.js.map +1 -1
  101. package/dist-server/service/reference/reference-resolver.d.ts +33 -3
  102. package/dist-server/service/reference/reference-resolver.js +290 -23
  103. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  104. package/dist-server/service/reference/template-registry.d.ts +19 -3
  105. package/dist-server/service/reference/template-registry.js.map +1 -1
  106. package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
  107. package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
  108. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  109. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  110. package/dist-server/service/twin-audit/command-audit.js +53 -0
  111. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  112. package/dist-server/service/twin-audit/index.d.ts +4 -0
  113. package/dist-server/service/twin-audit/index.js +8 -0
  114. package/dist-server/service/twin-audit/index.js.map +1 -0
  115. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  116. package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
  117. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  118. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  119. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  120. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  121. package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
  122. package/dist-server/service/twin-control/twin-control-mutation.js +50 -11
  123. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  124. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  125. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  126. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  127. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  128. package/dist-server/service/twin-event/twin-event.js +46 -14
  129. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  130. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  131. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  132. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  133. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  134. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  135. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  136. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  137. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  138. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  139. package/dist-server/service/twin-forecast/twin-forecast-query.js +219 -39
  140. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  141. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  142. package/dist-server/service/twin-instance/twin-instance.js +66 -18
  143. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  144. package/dist-server/service/twin-journal/twin-journal-query.d.ts +11 -4
  145. package/dist-server/service/twin-journal/twin-journal-query.js +95 -15
  146. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  147. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +11 -5
  148. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +46 -27
  149. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  150. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  151. package/dist-server/service/twin-metrics/twin-metrics-query.js +63 -1
  152. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  153. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  154. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  155. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  157. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  158. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  159. package/dist-server/service/twin-model/index.d.ts +13 -0
  160. package/dist-server/service/twin-model/index.js +29 -0
  161. package/dist-server/service/twin-model/index.js.map +1 -0
  162. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  163. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  164. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  165. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  166. package/dist-server/service/twin-model/project-structure.js +173 -0
  167. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  168. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  169. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  170. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  172. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  173. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  176. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  178. package/dist-server/service/twin-model/twin-location.js +130 -0
  179. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  180. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  181. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  182. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  183. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  184. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  185. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  186. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  187. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  188. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  189. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  190. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  191. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  192. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  193. package/dist-server/service/twin-model/twin-operation.js +137 -0
  194. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  195. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  196. package/dist-server/service/twin-space/move-space.js +166 -0
  197. package/dist-server/service/twin-space/move-space.js.map +1 -0
  198. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  199. package/dist-server/service/twin-space/space-integrity.js +182 -0
  200. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  201. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  202. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  203. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  204. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  206. package/dist-server/service/twin-space/twin-space-resolver.js +365 -18
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  209. package/dist-server/service/twin-space/twin-space.js +15 -5
  210. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  211. package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
  212. package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
  213. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  214. package/dist-server/service/twin-structure/twin-structure.js +8 -7
  215. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  216. package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
  217. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  218. package/dist-shared/axis-read.d.ts +12 -0
  219. package/dist-shared/axis-read.js +32 -0
  220. package/dist-shared/axis-read.js.map +1 -0
  221. package/dist-shared/entity-delta.d.ts +29 -0
  222. package/dist-shared/entity-delta.js +287 -0
  223. package/dist-shared/entity-delta.js.map +1 -0
  224. package/dist-shared/kpi-broadcast.d.ts +4 -0
  225. package/dist-shared/kpi-broadcast.js +16 -0
  226. package/dist-shared/kpi-broadcast.js.map +1 -0
  227. package/dist-shared/twin-level.d.ts +23 -0
  228. package/dist-shared/twin-level.js +52 -0
  229. package/dist-shared/twin-level.js.map +1 -0
  230. package/package.json +14 -12
  231. package/server/engine/attention-digest.ts +102 -0
  232. package/server/engine/canonical-ingest.ts +36 -4
  233. package/server/engine/command-routing.ts +67 -0
  234. package/server/engine/energy-topology.ts +189 -0
  235. package/server/engine/index.ts +17 -0
  236. package/server/engine/kpi-baseline.ts +202 -0
  237. package/server/engine/kpi-fold.ts +389 -1
  238. package/server/engine/kpi-query.ts +295 -22
  239. package/server/engine/kpi-target.ts +24 -2
  240. package/server/engine/live-attentions.ts +7 -2
  241. package/server/engine/live-feed-registry.ts +58 -0
  242. package/server/engine/load-meter.ts +384 -0
  243. package/server/engine/local-declarations.ts +700 -0
  244. package/server/engine/model-basis.ts +78 -10
  245. package/server/engine/model-vocabulary.ts +82 -0
  246. package/server/engine/oee-accumulator.ts +34 -1
  247. package/server/engine/operation-basis.ts +100 -0
  248. package/server/engine/property-effects.ts +199 -0
  249. package/server/engine/runtime-key.ts +58 -0
  250. package/server/engine/spec-coverage.ts +3 -3
  251. package/server/engine/state-axes.ts +60 -0
  252. package/server/engine/structure-diff.ts +1 -1
  253. package/server/engine/travel-estimator.ts +11 -6
  254. package/server/engine/twin-engine.ts +1710 -297
  255. package/server/engine/warm-start.ts +207 -22
  256. package/server/index.ts +13 -10
  257. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  258. package/server/migrations/index.ts +7 -0
  259. package/server/service/index.ts +9 -1
  260. package/server/service/reference/discovery-result.ts +95 -0
  261. package/server/service/reference/ingest-space.ts +104 -0
  262. package/server/service/reference/knob-defaults.ts +59 -0
  263. package/server/service/reference/reference-live.ts +121 -6
  264. package/server/service/reference/reference-master.ts +362 -35
  265. package/server/service/reference/reference-resolver.ts +307 -23
  266. package/server/service/reference/template-registry.ts +20 -5
  267. package/server/service/twin-attention/twin-attention-query.ts +43 -6
  268. package/server/service/twin-audit/command-audit.ts +81 -0
  269. package/server/service/twin-audit/index.ts +5 -0
  270. package/server/service/twin-audit/twin-audit-event.ts +112 -0
  271. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  272. package/server/service/twin-control/twin-control-mutation.ts +53 -13
  273. package/server/service/twin-event/twin-event-keys.ts +1 -1
  274. package/server/service/twin-event/twin-event.ts +52 -15
  275. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  276. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  277. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  278. package/server/service/twin-forecast/twin-forecast-query.ts +211 -38
  279. package/server/service/twin-instance/twin-instance.ts +127 -22
  280. package/server/service/twin-journal/twin-journal-query.ts +108 -14
  281. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +40 -23
  282. package/server/service/twin-metrics/twin-metrics-query.ts +60 -3
  283. package/server/service/twin-model/epcis-coverage.ts +195 -0
  284. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  285. package/server/service/twin-model/index.ts +25 -0
  286. package/server/service/twin-model/isa95-coverage.ts +190 -0
  287. package/server/service/twin-model/project-structure.ts +199 -0
  288. package/server/service/twin-model/standard-coverage.ts +35 -0
  289. package/server/service/twin-model/twin-equipment.ts +150 -0
  290. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  291. package/server/service/twin-model/twin-location.ts +140 -0
  292. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  293. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  294. package/server/service/twin-model/twin-model-query.ts +537 -0
  295. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  296. package/server/service/twin-model/twin-operation.ts +150 -0
  297. package/server/service/twin-space/move-space.ts +262 -0
  298. package/server/service/twin-space/space-integrity.ts +293 -0
  299. package/server/service/twin-space/twin-space-area.ts +4 -4
  300. package/server/service/twin-space/twin-space-representation.ts +6 -6
  301. package/server/service/twin-space/twin-space-resolver.ts +365 -22
  302. package/server/service/twin-space/twin-space.ts +40 -6
  303. package/server/service/twin-state/twin-state-subscription.ts +1 -1
  304. package/server/service/twin-structure/twin-structure.ts +10 -7
  305. package/server/service/twin-target/twin-target-resolver.ts +22 -4
  306. package/shared/axis-read.ts +28 -0
  307. package/shared/entity-delta.ts +286 -0
  308. package/shared/kpi-broadcast.ts +13 -0
  309. package/shared/twin-level.ts +48 -0
  310. package/test/adopt-structure-live.test.ts +133 -0
  311. package/test/attention-digest.test.ts +135 -0
  312. package/test/axis-read.test.ts +87 -0
  313. package/test/boot-resume.test.ts +205 -0
  314. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  315. package/test/capability-mapping.test.ts +5 -5
  316. package/test/command-routing.test.ts +61 -0
  317. package/test/declaration-reaches-model.test.ts +178 -0
  318. package/test/discovery-result.test.ts +75 -0
  319. package/test/energy-topology.test.ts +113 -0
  320. package/test/entity-delta.test.ts +195 -25
  321. package/test/event-time-column.test.ts +67 -0
  322. package/test/forecast-metrics.test.ts +71 -0
  323. package/test/forecast-tuning.test.ts +42 -0
  324. package/test/gap-analytics.test.ts +34 -7
  325. package/test/ingest-bench.test.ts +9 -9
  326. package/test/ingest-running-guard.test.ts +136 -0
  327. package/test/ingest-space.test.ts +78 -0
  328. package/test/instance-cache-lifecycle.test.ts +110 -0
  329. package/test/kernel-kind-guard.test.ts +96 -0
  330. package/test/knob-defaults.test.ts +72 -0
  331. package/test/kpi-baseline-db.test.ts +216 -0
  332. package/test/kpi-baseline.test.ts +196 -0
  333. package/test/kpi-fold.test.ts +393 -2
  334. package/test/kpi-query-bench.test.ts +130 -0
  335. package/test/lineage-survives-restart.test.ts +203 -0
  336. package/test/live-feed-registry.test.ts +67 -0
  337. package/test/live-kernel-facts.test.ts +161 -0
  338. package/test/live-mirror-parity.test.ts +54 -14
  339. package/test/load-meter.test.ts +314 -0
  340. package/test/local-declarations.test.ts +554 -0
  341. package/test/master-to-twin.test.ts +128 -36
  342. package/test/model-basis.test.ts +56 -1
  343. package/test/model-vocabulary.test.ts +114 -0
  344. package/test/move-space.test.ts +149 -0
  345. package/test/mutation-gate.test.ts +122 -0
  346. package/test/oee-accumulator.test.ts +90 -7
  347. package/test/operation-basis.test.ts +100 -0
  348. package/test/operations-capability-db.test.ts +165 -0
  349. package/test/project-structure-db.test.ts +226 -0
  350. package/test/projection-reaches-screen.test.ts +272 -0
  351. package/test/property-effects.test.ts +96 -0
  352. package/test/registry-key-guard.test.ts +80 -0
  353. package/test/resync-origin-site.test.ts +77 -0
  354. package/test/runtime-key.test.ts +66 -0
  355. package/test/scale-twin-bench.test.ts +7 -7
  356. package/test/snapshot-freshness.test.ts +60 -0
  357. package/test/space-integrity.test.ts +223 -0
  358. package/test/standard-coverage.test.ts +94 -0
  359. package/test/state-axes.test.ts +74 -0
  360. package/test/streamline-e2e.test.ts +16 -16
  361. package/test/structure-revision-db.test.ts +16 -15
  362. package/test/tenant-registry-db.test.ts +149 -0
  363. package/test/twin-audit.test.ts +83 -0
  364. package/test/twin-model-item-db.test.ts +275 -0
  365. package/test/twin-model-tree-db.test.ts +176 -0
  366. package/test/twin-origin-resync.test.ts +114 -0
  367. package/test/warm-start-seam.test.ts +140 -0
  368. package/test/warm-start.test.ts +223 -4
  369. package/tsconfig.json +6 -1
  370. package/tsconfig.shared.json +23 -0
  371. package/tsconfig.shared.tsbuildinfo +1 -0
  372. package/tsconfig.tsbuildinfo +1 -0
  373. package/dist-server/engine/entity-delta.d.ts +0 -19
  374. package/dist-server/engine/entity-delta.js +0 -161
  375. package/dist-server/engine/entity-delta.js.map +0 -1
  376. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  377. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  378. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  379. package/dist-server/tsconfig.tsbuildinfo +0 -1
  380. package/server/engine/entity-delta.ts +0 -169
  381. package/server/service/twin-event/backfill-keys.ts +0 -72
@@ -0,0 +1,203 @@
1
+ /*
2
+ * ★ 재기동을 건너도 **계보에 입력이 남는다** — 호스트의 웜스타트 경로로 확인한다.
3
+ *
4
+ * ── 무엇이 잘못돼 있었나 ──────────────────────────────────────────────────────
5
+ * 오더의 진행 중 할당(`allocated`)과 거래번호(`bizTransaction`)가 상태 계약에 없어서, 웜스타트가
6
+ * 오더를 되살릴 때 둘 다 비운 채로 세웠다. 그 오더의 진행 중 작업이 완료되면
7
+ * `TransformationEvent` 가 **입력 없이** 나간다 — "무엇이 무엇으로 바뀌었나" 에서 "무엇이" 가
8
+ * 통째로 빠지고, 계보를 거슬러 올라갈 수 없다.
9
+ *
10
+ * 실 앱에서 변환 36건 중 35건이 입력 0 이었고 모든 이벤트의 거래번호가 빈값이었다.
11
+ *
12
+ * ── 왜 커널 검사만으로 충분하지 않나 ──────────────────────────────────────────
13
+ * 커널 검사는 `hydrateObserved` 로 직접 심어 확인한다. 호스트는 **저널을 재생해서** 그 씨앗을
14
+ * 만든다(`recover` → `replay`). 그 경로에서 한 번 더 떨어질 수 있으므로 여기서 실제로 걷는다.
15
+ */
16
+ import { test, before, after } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { createRequire } from 'node:module'
19
+ import type { DataSource } from 'typeorm'
20
+
21
+ const req = createRequire(import.meta.url)
22
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
23
+ const { entities: authEntities } = req('@things-factory/auth-base')
24
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
25
+ const { TwinEngine } = req('../dist-server/engine/index.js')
26
+ const { TwinEvent } = req('../dist-server/service/twin-event/twin-event.js')
27
+ const { TwinLineageQuery } = req('../dist-server/service/twin-model/twin-lineage-query.js')
28
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
29
+
30
+ let ds: DataSource
31
+ let domainId: string
32
+
33
+ const INSTANCE = 'lineage-probe'
34
+ const CP = '0614141'
35
+ const pat = (ref: string) => `urn:epc:idpat:sgtin:${CP}.${ref}.*`
36
+
37
+ /** 원자재 하나로 완제품 하나를 만드는 2단 라인 — 실제 트레일러 템플릿의 축소판. */
38
+ const master = (): any => ({
39
+ source: INSTANCE,
40
+ system: 'mes',
41
+ siteName: '계보 공장',
42
+ space: { width: 60, depth: 30, unit: 'm' },
43
+ locations: [
44
+ { id: 'raw-store', type: 'raw-store', role: 'location', capacity: 500 },
45
+ { id: 'cut-station', type: 'cut-station', role: 'location', capacity: 50 },
46
+ { id: 'weld-station', type: 'weld-station', role: 'location', capacity: 50 },
47
+ { id: 'fg-store', type: 'fg-store', role: 'location', capacity: 500 }
48
+ ],
49
+ equipment: [
50
+ { id: 'cutter-1', kind: 'cutter', homeLocationId: 'cut-station' },
51
+ { id: 'welder-1', kind: 'welder', homeLocationId: 'weld-station' }
52
+ ],
53
+ productionSpec: {
54
+ companyPrefix: CP,
55
+ binding: { steel: '077101', frame: '078101' },
56
+ definition: {
57
+ materials: [{ id: 'steel' }, { id: 'frame' }],
58
+ routes: [{ key: 'frame-route', steps: ['cut', 'weld'] }],
59
+ recipes: [{ key: 'frame-recipe', label: 'f', route: 'frame-route', inputs: [{ material: 'steel', qty: 2 }], outputs: [{ material: 'frame', qty: 1 }] }],
60
+ operations: [
61
+ { key: 'cut', label: '절단', intent: 'process', locationType: 'cut-station', resourceType: 'cutter', duration: 'PT1M' },
62
+ { key: 'weld', label: '용접', intent: 'process', locationType: 'weld-station', resourceType: 'welder', duration: 'PT1M' }
63
+ ]
64
+ }
65
+ }
66
+ })
67
+
68
+ const SCENARIO = {
69
+ seed: 9,
70
+ generators: [
71
+ { kind: 'inbound-arrival', stimulus: 'arrival', rate: { distribution: 'constant', meanPerHour: 3600 },
72
+ content: { skuMix: [{ gtin: pat('077101'), weight: 1 }], qtyPerLine: { min: 1, max: 1 } } },
73
+ { kind: 'production-order', stimulus: 'order', rate: { distribution: 'constant', meanPerHour: 120 },
74
+ content: { skuMix: [{ gtin: pat('078101'), weight: 1 }], qtyPerLine: { min: 1, max: 1 } } }
75
+ ]
76
+ }
77
+
78
+ /** 저널에 남은 변환 이벤트들 — 계보가 실제로 무엇을 기록했나. */
79
+ async function transforms(): Promise<{ inputs: number; outputs: number; tx: string }[]> {
80
+ const rows: any[] = await ds.getRepository(TwinEvent).find({ where: { domain: { id: domainId } as any, instanceId: INSTANCE } as any })
81
+ return rows
82
+ .filter(r => r.eventType === 'epcis.TransformationEvent')
83
+ .map(r => ({
84
+ inputs: (r.payload?.data?.inputEPCList ?? []).length,
85
+ outputs: (r.payload?.data?.outputEPCList ?? []).length,
86
+ tx: r.payload?.data?.bizTransactionList?.[0]?.bizTransaction ?? ''
87
+ }))
88
+ }
89
+
90
+ /**
91
+ * 틱과 자극은 **런타임**에 있다 — `startFromRegistry` 가 돌려주는 것은 호스트의 인스턴스 기록이고
92
+ * 커널 런타임은 그 안에 있다(둘을 같은 것으로 보면 `scenario` 가 없다고 나온다).
93
+ */
94
+ const runtime = () => TwinEngine.runtime(domainId, INSTANCE)
95
+
96
+ function tick(n: number): void {
97
+ const rt = runtime()
98
+ for (let i = 0; i < n; i++) rt.tick(1000)
99
+ }
100
+
101
+ /**
102
+ * 상태는 런타임의 `resync()` 로 얻는다 — `{ revision, state }` 를 돌려주므로 축은 그 **안에** 있다.
103
+ * (`TwinEngine.snapshot` 도 이것을 그대로 넘겨 주므로 축이 한 겹 안에 들어 있다.)
104
+ */
105
+ const orders = () => ((runtime().resync().state?.orders ?? []) as any[])
106
+
107
+ function drive(): void {
108
+ const rt = runtime()
109
+ rt.scenario.load(SCENARIO)
110
+ rt.scenario.start()
111
+ }
112
+
113
+ before(async () => {
114
+ ds = new TypeOrmDataSource({
115
+ type: 'sqlite',
116
+ database: ':memory:',
117
+ synchronize: true,
118
+ dropSchema: true,
119
+ namingStrategy: new NamingStrategy(),
120
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
121
+ logging: false
122
+ })
123
+ await ds.initialize()
124
+ addDataSource('default', ds)
125
+ const d = await ds.getRepository(Domain).save({ name: 'lineage-test', subdomain: 'lineage-test' } as any)
126
+ domainId = d.id
127
+ await TwinEngine.ingestMaster(domainId, master())
128
+ })
129
+
130
+ after(async () => {
131
+ await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
132
+ await ds?.destroy()
133
+ })
134
+
135
+ test('재기동을 건너도 계보에 입력이 실린다 — 웜스타트가 확보분을 잃지 않는다', async () => {
136
+ /* ① 한 번 굴려 진행 중 할당을 만든다. */
137
+ await TwinEngine.startFromRegistry(domainId, INSTANCE, 'sim-world')
138
+ drive()
139
+ tick(200)
140
+ const workingBefore = orders().filter(o => (o.allocated ?? []).length)
141
+ assert.ok(workingBefore.length, '진행 중 확보분이 있어야 이 검사가 뜻이 있다')
142
+
143
+ /* ② 멈추고 **저널을 재생해** 다시 세운다 — 호스트의 웜스타트 경로 그대로. */
144
+ await TwinEngine.stop(domainId, INSTANCE)
145
+ await TwinEngine.startFromRegistry(domainId, INSTANCE, 'sim-world')
146
+
147
+ const revived = orders().filter(o => (o.allocated ?? []).length)
148
+ assert.ok(revived.length, '되살아난 오더가 확보분을 들고 있어야 한다 — 잃으면 계보의 입력이 빈다')
149
+
150
+ /* ③ 되살아난 작업이 끝날 만큼 더 굴린다. */
151
+ drive()
152
+ tick(300)
153
+
154
+ const all = await transforms()
155
+ assert.ok(all.length, '변환이 저널에 남아야 이 검사가 뜻이 있다')
156
+ const withInputs = all.filter(t => t.inputs > 0)
157
+ assert.ok(withInputs.length, '입력이 실린 변환이 하나도 없다 — 계보의 "어디서 왔나" 가 통째로 빈다')
158
+
159
+ /* 거래번호도 함께 — 계보가 **무슨 거래로** 일어났는지 잇는 축이다. */
160
+ assert.ok(all.some(t => t.tx), '거래번호가 전부 빈값이다 — 계보를 거래로 이을 수 없다')
161
+ })
162
+
163
+ test('계보를 거슬러 올라가 선언과 나란히 놓는다 — 그 차이가 진단이다', async () => {
164
+ const q = new TwinLineageQuery()
165
+ const ctx = { state: { domain: { id: domainId } } } as any
166
+
167
+ /* 완제품 하나를 고른다 — 저널이 그것을 산출로 실은 변환이 있어야 계보가 있다. */
168
+ const rows: any[] = await ds.getRepository(TwinEvent).find({ where: { domain: { id: domainId } as any, instanceId: INSTANCE } as any })
169
+ /* **완제품**을 고른다. 변환에는 중간 산물(WIP)을 내는 것도 있어서 아무거나 집으면 WIP 이 잡히고,
170
+ WIP 은 선언된 자재가 아니라 라우트의 내부 산물이므로 견줄 대상이 아니다. */
171
+ const made = rows
172
+ .filter(r => r.eventType === 'epcis.TransformationEvent' && (r.payload?.data?.inputEPCList ?? []).length)
173
+ .flatMap(r => r.payload.data.outputEPCList ?? [])
174
+ .filter((e: string) => e.includes(`.078101.`))
175
+ assert.ok(made.length, '완제품을 낸 변환이 있어야 이 검사가 뜻이 있다')
176
+
177
+ const lin = await q.twinItemLineage(INSTANCE, made[0], ctx)
178
+ assert.ok(lin, '계보가 나와야 한다')
179
+ assert.equal(lin.epc, made[0])
180
+ assert.ok(lin.items.length > 1, '거슬러 올라간 것이 있어야 한다 — 하나면 아무것도 안 찾은 것이다')
181
+ assert.ok(lin.edges.some((e: any) => e.from === made[0]), '이 개체를 만든 직접 투입이 보여야 한다')
182
+
183
+ /* 근거를 밝힌다 — 창을 넘겨 끊겼으면 계보가 실제보다 얕게 보인다. */
184
+ assert.equal(lin.basis.source, 'journal')
185
+ assert.equal(typeof lin.basis.transformationsRead, 'number')
186
+
187
+ /* **겹쳐 보기** — 선언(레시피)과 관측(실제 투입)을 자재별로 나란히. */
188
+ assert.equal(lin.conformance.comparable, true, `견줄 수 없으면 이유를 말한다: ${lin.conformance?.reason}`)
189
+ const steel = lin.conformance.lines.find((l: any) => l.material === 'steel')
190
+ assert.ok(steel, '레시피가 선언한 자재가 줄로 나와야 한다')
191
+ assert.equal(steel.declared, 2, '레시피는 강재 2개를 요구한다')
192
+ assert.equal(steel.observed, 2, '실제로도 2개를 먹었다 — 설계대로다')
193
+ assert.equal(steel.matches, true)
194
+ })
195
+
196
+ test('바인딩이 없으면 비교하지 않고 이유를 알린다 — 짐작으로 이으면 거짓 진단이 된다', async () => {
197
+ const q = new TwinLineageQuery()
198
+ const ctx = { state: { domain: { id: domainId } } } as any
199
+ /* 개체이긴 하지만 바인딩에 없는 품목 — 짐작으로 자재에 붙이면 "설계와 다르다" 는 거짓이 나온다. */
200
+ const lin = await q.twinItemLineage(INSTANCE, `urn:epc:id:sgtin:${CP}.099999.1`, ctx)
201
+ assert.equal(lin.conformance.comparable, false)
202
+ assert.equal(lin.conformance.reason, 'item-not-bound')
203
+ })
@@ -0,0 +1,67 @@
1
+ /*
2
+ * 「계측이 들어오고 있나」 — **도는 것과 받는 것을 가른다.**
3
+ *
4
+ * ── 무엇이 보이지 않았나 (2026-08-14) ───────────────────────────────────────
5
+ * 미러 트윈은 커널이 돌기만 하면 「도는 중」이었다. 피드가 떨어져 있으면 그 트윈은 아무것도 받지
6
+ * 않으면서 도는 중인데, 화면에서 두 상태가 똑같이 보였다 — 재기동 뒤 실제로 그랬고 사람은
7
+ * 「값이 안 변하네」로 뒤늦게 알았다.
8
+ *
9
+ * 이 모듈은 DB 를 물지 않는다 — 그래서 배선의 모양이 아니라 **거동**을 시험한다.
10
+ */
11
+ import { test } from 'node:test'
12
+ import assert from 'node:assert/strict'
13
+
14
+ import { registerLiveFeedProbe, liveFeedAttached, liveFeedStateOf } from '../server/engine/live-feed-registry.ts'
15
+
16
+ /* 두 계층의 장부를 흉내낸다 — 커넥터 기반과 프리셋 기반. */
17
+ const connectorFeeds = new Set<string>()
18
+ const presetFeeds = new Set<string>()
19
+ registerLiveFeedProbe(() => [...connectorFeeds])
20
+ registerLiveFeedProbe(() => [...presetFeeds])
21
+
22
+ test('어느 장부든 붙어 있다고 하면 붙어 있다 — 한쪽만 보면 흐르는 트윈을 끊겼다고 부른다', () => {
23
+ connectorFeeds.add('ems-plant-1')
24
+ presetFeeds.add('sap-wh-1')
25
+
26
+ assert.equal(liveFeedAttached('ems-plant-1'), true, '커넥터 기반 피드')
27
+ assert.equal(liveFeedAttached('sap-wh-1'), true, '프리셋 기반 피드 — 이걸 놓치면 거짓 경보가 된다')
28
+ assert.equal(liveFeedAttached('nobody'), false)
29
+ })
30
+
31
+ test('답하지 못하는 장부가 다른 계층의 사실을 죽이지 않는다', () => {
32
+ registerLiveFeedProbe(() => {
33
+ throw new Error('registry down')
34
+ })
35
+ /* 터진 장부 뒤에 등록된 장부의 사실도 그대로 읽혀야 한다. */
36
+ const late = new Set<string>(['late-twin'])
37
+ registerLiveFeedProbe(() => [...late])
38
+ assert.equal(liveFeedAttached('late-twin'), true)
39
+ assert.equal(liveFeedAttached('ems-plant-1'), true)
40
+ })
41
+
42
+ test('「없음」과 「해당 없음」을 섞지 않는다', () => {
43
+ /* 도는 미러 — 이 둘이 우리가 보이려던 구별이다. */
44
+ assert.equal(liveFeedStateOf({ realityMode: 'mirror', running: true, instanceId: 'ems-plant-1' }), 'attached')
45
+ assert.equal(liveFeedStateOf({ realityMode: 'mirror', running: true, instanceId: 'ems-plant-2' }), 'detached')
46
+
47
+ /* 시뮬 트윈에는 나를 피드가 **없어야 한다** — 결함이 아니라 그 트윈의 성질이다. */
48
+ assert.equal(liveFeedStateOf({ realityMode: 'sim-world', running: true, instanceId: 'hatio-us' }), 'not-applicable')
49
+ assert.equal(liveFeedStateOf({ realityMode: 'sim-experiment', running: true, instanceId: 'trailer-new' }), 'not-applicable')
50
+
51
+ /* 사람이 멈춰 둔 트윈에게 피드를 묻는 것은 뜻이 없다. */
52
+ assert.equal(liveFeedStateOf({ realityMode: 'mirror', running: false, instanceId: 'ems-plant-1' }), 'not-applicable')
53
+
54
+ /* 선언이 없으면 미러라고 짐작하지 않는다(짐작으로 경보를 울리지 않는다). */
55
+ assert.equal(liveFeedStateOf({ running: true, instanceId: 'ems-plant-1' }), 'not-applicable')
56
+ })
57
+
58
+ test('목록이 그 사실을 함께 낸다 — 화면이 따로 물어야 하면 언젠가 묻지 않는다', async () => {
59
+ const src = await import('node:fs').then(fs =>
60
+ fs.readFileSync(new URL('../server/engine/twin-engine.ts', import.meta.url), 'utf-8')
61
+ )
62
+ const at = src.indexOf('static async list(')
63
+ assert.ok(at >= 0, 'list() 가 없다 — 이름이 바뀌었으면 이 검사도 함께 고친다')
64
+ const body = src.slice(at, src.indexOf('\n /** 운영 공간 목록', at))
65
+ assert.match(body, /liveFeed: liveFeedStateOf\(/, '관리 목록 행에 피드 사실이 실려야 한다')
66
+ assert.match(body, /realityMode: r\.realityMode/, '선언된 모드를 넘겨야 한다(짐작하지 않는다)')
67
+ })
@@ -0,0 +1,161 @@
1
+ /*
2
+ * 라이브에서 **커널이 낸 사실**이 사라지지 않는다 — 저널에 남고, 인입은 두 번 적히지 않는다.
3
+ *
4
+ * ── 실측 (2026-08-14) ───────────────────────────────────────────────────────
5
+ * 에너지 트윈에 커넥터를 붙여 표본을 흘렸다. 저널에 `energy.measured` 48건이 쌓였는데 **구간 마감은
6
+ * 0건**이었다. 커널은 15분 경계에서 마감 사실을 내고 있었지만(`emitOp`) 라이브 경로에 그것을 구독하는
7
+ * 곳이 없어서 **커널 안에서 사라졌다.** 저널을 읽는 성과 화면의 전력 타일은 영원히 나오지 않는다.
8
+ *
9
+ * sim 은 `runtime.subscribe` 로 같은 일을 한다 — 라이브에만 그 배선이 없었다.
10
+ *
11
+ * 이 시험이 지키는 것 둘:
12
+ * ① 라이브 기동은 커널 사건을 구독한다(그 사실이 저널로 간다)
13
+ * ② 인입 봉투는 **한 번만** 적힌다 — `apply()` 가 그것을 구독자에게 되흘리기 때문이다
14
+ *
15
+ * 엔진은 DB·shell 을 물고 있어 단위 시험이 불러올 수 없다. 그래서 **배선의 모양**을 소스로 지킨다 —
16
+ * 실제 거동은 실서버에서 확인했고(그때 이 결함이 드러났다), 여기서는 그 배선이 사라지지 않게 한다.
17
+ */
18
+ import { test } from 'node:test'
19
+ import assert from 'node:assert/strict'
20
+ import { readFileSync } from 'node:fs'
21
+ import { fileURLToPath } from 'node:url'
22
+
23
+ const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
24
+
25
+ /** 그 정적 메서드의 본문만 — 다음 메서드 선언까지. `async` 도 찾는다(빠뜨리면 있는 메서드를 없다고 한다). */
26
+ function methodOf(name: string): string {
27
+ const at = [`static ${name}(`, `static async ${name}(`].map(sig => src.indexOf(sig)).find(i => i >= 0) ?? -1
28
+ assert.ok(at >= 0, `${name} 가 없다 — 이름이 바뀌었으면 이 검사도 함께 고친다`)
29
+ const next = src.indexOf('\n static ', at + 10)
30
+ return src.slice(at, next < 0 ? undefined : next)
31
+ }
32
+
33
+ test('라이브 기동이 커널 사건을 구독한다 — 판정이 저널로 가는 유일한 길', () => {
34
+ const body = methodOf('startLive')
35
+ assert.match(body, /kernel\.onEvent\?\.\(/, '구독이 없으면 커널의 마감·피크·제안이 사라진다')
36
+ assert.match(body, /pendingJournal/, '구독한 사실은 저널 배치로 가야 한다')
37
+ /* 예전에는 `unsub: () => {}` 로 아무것도 구독하지 않았다 — 그 모양으로 되돌아가지 않게 못 박는다. */
38
+ assert.equal(
39
+ /unsub: \(\) => \{\}[\s\S]{0,80}$/.test(body.trim()),
40
+ false,
41
+ 'startLive 가 아무것도 구독하지 않는 모양으로 되돌아갔다'
42
+ )
43
+ })
44
+
45
+ test('인입 봉투는 저널에 한 번만 적힌다 — apply 가 그것을 되흘린다', () => {
46
+ const start = methodOf('startLive')
47
+ const ingest = methodOf('ingestLive')
48
+ /* 표시하고(ingest) 걸러낸다(구독). 둘 중 하나만 있으면 인입이 두 번 적히거나 표시가 무의미해진다. */
49
+ assert.match(ingest, /applying\?\.add\(/, '인입 봉투를 표시해야 한다')
50
+ assert.match(start, /applying\.has\(/, '표시된 봉투는 구독에서 건너뛰어야 한다')
51
+ })
52
+
53
+ test('식별자가 아니라 **객체 동일성**으로 가른다 — 어댑터의 id 규칙에 기대지 않게', () => {
54
+ const start = methodOf('startLive')
55
+ assert.match(start, /WeakSet<object>/, '약한 참조로 표시한다(따로 비울 것이 없다)')
56
+ /* eventId 비교는 어댑터가 id 를 어떻게 만드는지에 기대게 되어 약하다 — 그 방식으로 되돌아가지 않게. */
57
+ assert.equal(/applying\.has\(e\.eventId\)/.test(start), false, 'id 비교로 되돌아갔다')
58
+ })
59
+
60
+ test('커널이 낸 사실도 방송을 예약한다 — 피크·마감은 상태를 바꾼다', () => {
61
+ const body = methodOf('startLive')
62
+ assert.match(body, /inst\.dirty = true/, '상태가 바뀌었으므로 다음 주기에 방송해야 한다')
63
+ assert.match(body, /ensureBroadcastCoalescer\(\)/, '방송은 주기로 합친다(사건마다 방송하면 폭발한다)')
64
+ })
65
+
66
+ /*
67
+ * ── 구조 전환도 방송한다 (2026-08-18) ───────────────────────────────────────
68
+ * 화면이 보는 것 상당수가 구조에서 파생된다(계약 대비 판정·주목 신호·자리 색). 구조 전환이 조용히
69
+ * 끝나면 지도 레일은 다음 계측 표본이 올 때까지 옛 화면을 들고 있고, 헤더 배지는 폴링으로 먼저 알아
70
+ * 「배지엔 있고 목록엔 없는」 어긋난 화면이 된다(실화면에서 봤다).
71
+ */
72
+ test('구조 전환은 상태 방송을 예약한다 — 다음 표본까지 화면이 옛것을 들고 있지 않게', () => {
73
+ const body = methodOf('adoptStructure')
74
+ assert.match(body, /inst\.dirty = true/, '구조가 바뀐 순간이 상태가 바뀐 순간이다')
75
+ assert.match(body, /ensureBroadcastCoalescer\(\)/, '새 경로가 아니라 있는 병합 규율에 얹는다')
76
+ /* 커널 전환보다 먼저 방송하면 구독자가 옛 스냅샷을 되물어 간다 — 순서가 규율이다. */
77
+ assert.ok(
78
+ body.indexOf('inst.kernel.adoptStructure(model)') < body.indexOf('inst.dirty = true'),
79
+ '커널을 갈고 나서 방송한다'
80
+ )
81
+ })
82
+
83
+ /*
84
+ * ── 자를 것을 알면서 다 읽어 오지 않는다 (2026-08-18 실측) ───────────────────
85
+ * 저널을 전량 읽어 온 뒤 JS 에서 걸렀다. 27만 건짜리 트윈에서 한 시간 전 상태를 물으면 7.4초가 걸렸고,
86
+ * 그 대부분이 「필요 없는 행을 읽어 오는 시간」이었다. 시각·리비전 상한은 SQL 이 아는 조건이다.
87
+ *
88
+ * 시간 측정은 도는 서버에서 크게 흔들리므로(같은 질의 596~1547ms) **배선**을 지킨다.
89
+ */
90
+ test('저널 상한은 SQL 에서 자른다 — 읽고 나서 버리지 않는다', () => {
91
+ const body = methodOf('recover')
92
+ assert.match(body, /eventTime: LessThanOrEqual\(new Date\(cutoffMs\)\)/, '시각 상한은 질의 조건이다')
93
+ assert.match(body, /revision: LessThanOrEqual\(untilRevision\)/, '리비전 상한도 질의 조건이다')
94
+ /* 시각이 비어 있는 옛 행은 함께 가져온다 — SQL 로 자르면 그 행들이 빠져 예전과 다른 결과가 된다. */
95
+ assert.match(body, /eventTime: IsNull\(\)/, '시각 없는 레거시 행을 잃지 않는다')
96
+ /* 읽어 온 뒤 다시 거르지 않는다 — 두 벌이 되면 어느 쪽이 진짜 조건인지 흐려진다. */
97
+ assert.equal(/rows\.filter\(/.test(body), false, '읽고 나서 또 거르지 않는다')
98
+ })
99
+
100
+ test('과거 화면의 「지금」은 잘라 낸 뒤의 마지막 사실이다 — 미래 이벤트를 쓰지 않는다', () => {
101
+ const body = methodOf('recover')
102
+ /* 예전에는 전량(rows)의 마지막 시각을 썼다 — 물어본 시점보다 뒤의 이벤트라 과거 화면이 미래를
103
+ 기준으로 「늦었다」를 판정할 수 있었다. 이제 rows 는 이미 잘린 것이다. */
104
+ assert.match(body, /const wanted = rows/, '자른 것과 접는 것이 같아야 한다')
105
+ })
106
+
107
+ /*
108
+ * ── 재개점에서 이어 접는다 (2026-08-18) ──────────────────────────────────────
109
+ * 접은 결과는 이미 남기고 있었지만, 그 지점의 **재개점**(리듀서 내부 상태 전부 + 가동 누적기)이 없어서
110
+ * 이벤트가 하나만 늘어도 0부터 다시 접어야 했다. 동치는 커널 시험이 증명한다(0부터 == 재개점 + 꼬리).
111
+ * 여기서는 **호스트가 그 씨앗을 실제로 쓰는지**를 지킨다 — 배선이 사라지면 지름길은 조용히 사라진다.
112
+ */
113
+ test('폴드가 재개점을 쓰고, 새 재개점을 함께 남긴다', () => {
114
+ const body = methodOf('recover')
115
+ assert.match(body, /replayFrom\(model, resume\.fold\.reducer/, '씨앗이 있으면 꼬리만 접는다')
116
+ assert.match(body, /revision: MoreThan\(from\)/, '읽는 것도 그 뒤만')
117
+ assert.match(body, /oee\.restore\(resume\.fold\.oee\)/, '가동 누적기도 이어받는다(없으면 가용률이 작아진다)')
118
+ assert.match(body, /replayWithCheckpoint\(/, '씨앗이 없을 때도 새 재개점을 만든다')
119
+ assert.match(body, /keep\(withNow\(out\.state\), \{ reducer: out\.checkpoint, oee: oee\.serialize\(\) \}\)/, '재개점을 함께 저장한다')
120
+ })
121
+
122
+ test('씨앗을 좁게 쓴다 — 캐시가 사실을 이기지 않는다', () => {
123
+ const body = methodOf('recover')
124
+ /* 지금을 물었을 때만·구조가 그대로일 때만. 시간여행은 목표 이전 재개점이 필요하고(사슬은 다음 단계),
125
+ 구조가 바뀌면 그 경계에서 갈라 접어야 한다. */
126
+ assert.match(body, /asOfNowRead && tip && cachedForResume && cachedForResume\.fold/, '지금 읽기에만 쓴다')
127
+ assert.match(body, /\(cachedForResume\.structureRev \?\? null\) === tip\.structureRev/, '구조가 같을 때만 쓴다')
128
+ assert.match(body, /segments\.length <= 1/, '구조가 갈린 폴드에는 쓰지 않는다')
129
+ })
130
+
131
+ /*
132
+ * ── 재개점 **사슬** (2026-08-18) ─────────────────────────────────────────────
133
+ * 최신 재개점 하나로는 과거를 도울 수 없다 — 그것은 언제나 목표보다 뒤에 있다. 그래서 지점을 여러 개
134
+ * 남기고, 과거를 물으면 **목표 직전** 지점에서 접는다. 지점은 각각 상태 전체를 들고 있어 무겁기 때문에
135
+ * 눈금(리비전 간격)을 넘을 때만 남기고 최근 몇 개만 든다 — 더 과거는 0부터 접는다(알려진 한계).
136
+ */
137
+ test('과거 읽기는 목표 직전 지점에서 접는다 — 사슬을 쓴다', () => {
138
+ const body = methodOf('recover')
139
+ assert.match(body, /this\.chainSeedFor\(domainId, instanceId, \{/, '과거 씨앗을 사슬에서 고른다')
140
+ assert.match(body, /structureCount <= 1/, '구조가 갈린 트윈에서는 쓰지 않는다')
141
+ assert.match(body, /revision: And\(MoreThan\(from\), LessThanOrEqual\(untilRevision\)\)/, '씨앗 뒤 · 목표 이내만 읽는다')
142
+ assert.match(body, /this\.keepChainPoint\(domainId, instanceId, \{/, '지점을 남긴다')
143
+ })
144
+
145
+ test('시각으로 물으면 지점의 시각도 목표 이내여야 한다 — 짐작하지 않는다', () => {
146
+ const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
147
+ const at = src.indexOf('private static async chainSeedFor(')
148
+ const body = src.slice(at, src.indexOf('\n private static async keepChainPoint(', at))
149
+ assert.match(body, /if \(!Number\.isFinite\(t\) \|\| t > target\.timeMs\) continue/, '시각을 모르는 지점은 쓰지 않는다')
150
+ assert.match(body, /if \(!value\?\.fold\?\.reducer\) continue/, '재개점 없는 지점은 씨앗이 아니다')
151
+ })
152
+
153
+ test('사슬은 눈금을 넘을 때만 자라고, 최근 몇 개만 든다', () => {
154
+ const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
155
+ const at = src.indexOf('private static async keepChainPoint(')
156
+ const body = src.slice(at, src.indexOf('\n /** 이 트윈 저널의 끝 리비전', at))
157
+ assert.match(body, /value\.revision - newest < this\.CHAIN_STRIDE\) return/, '눈금을 넘지 않으면 남기지 않는다')
158
+ assert.match(body, /slice\(-this\.CHAIN_KEEP\)/, '최근 몇 개만 든다')
159
+ /* 공용 캐시에 삭제 API 가 없다는 사실을 숨기지 않는다 — 버린 지점은 TTL 까지 남는다. */
160
+ assert.match(body, /values expire with the cache TTL/, '버린 지점의 운명을 말한다')
161
+ })
@@ -13,7 +13,7 @@ import assert from 'node:assert/strict'
13
13
 
14
14
  import { WmsKernel, StateProjector, deriveAttentions, computeOee } from '@operato/twin-kernel'
15
15
  import { masterToTwin } from '../server/service/reference/reference-master.ts'
16
- import { buildEntityDeltas } from '../server/engine/entity-delta.ts'
16
+ import { buildEntityDeltas } from '../shared/entity-delta.ts'
17
17
  import { withLiveAttentions } from '../server/engine/live-attentions.ts'
18
18
 
19
19
  const GTIN = 'urn:epc:idpat:sgtin:0614141.099999.*'
@@ -36,17 +36,17 @@ const master = () => ({
36
36
 
37
37
  /** 커널 sim 구동 + 방출 이벤트 수집(커널 face2 테스트 패턴). */
38
38
  function runSim() {
39
- const board = masterToTwin(master()).board
39
+ const model = masterToTwin(master()).model
40
40
  const kernel = new WmsKernel('t')
41
41
  const events: any[] = []
42
42
  kernel.onEvent((e: any) => events.push(e))
43
- kernel.loadBoard(board)
43
+ kernel.loadTwinModel(model)
44
44
  kernel.scenario.load({ seed: 42, generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 120 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 1, max: 1 } } }] })
45
45
  kernel.scenario.start()
46
46
  for (let i = 0; i < 300; i++) kernel.tick(1000)
47
47
  kernel.scenario.pause()
48
48
  for (let i = 0; i < 60; i++) kernel.tick(1000)
49
- return { board, kernel, events }
49
+ return { model, kernel, events }
50
50
  }
51
51
 
52
52
  const sortByTag = (arr: any[]) => [...arr].sort((a, b) => (a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0))
@@ -72,13 +72,13 @@ const coreOnly = (d: any) => {
72
72
 
73
73
  /** sim(커널) · live(projector 재투영) payload 를 태그로 정렬해 나란히. */
74
74
  function bothPayloads() {
75
- const { board, kernel, events } = runSim()
75
+ const { model, kernel, events } = runSim()
76
76
  assert.ok(events.length > 0, 'sim 이벤트 생성됨')
77
- const proj = new StateProjector(board)
77
+ const proj = new StateProjector(model)
78
78
  for (const env of events) proj.apply(env)
79
79
  return {
80
- sim: sortByTag(buildEntityDeltas(kernel.getSnapshot())),
81
- live: sortByTag(buildEntityDeltas(proj.snapshot()))
80
+ sim: sortByTag(buildEntityDeltas(kernel.getSnapshot(), 't1')),
81
+ live: sortByTag(buildEntityDeltas(proj.snapshot(), 't1'))
82
82
  }
83
83
  }
84
84
 
@@ -91,7 +91,7 @@ test('★ 라이브 미러: 핵심 관측 상태(점유·용량·위치·아이
91
91
  const { sim, live } = bothPayloads()
92
92
  const simCore = sim.map(d => ({ tag: d.tag, data: coreOnly(d.data) }))
93
93
  const liveCore = live.map(d => ({ tag: d.tag, data: coreOnly(d.data) }))
94
- // 통로(라우팅)는 온전 — 이벤트로 재구성되는 관측 상태는 두 드라이버가 동일 → 보드가 동일 렌더.
94
+ // 통로(라우팅)는 온전 — 이벤트로 재구성되는 관측 상태는 두 드라이버가 동일 → 모델이 동일 렌더.
95
95
  assert.deepEqual(liveCore, simCore, 'occupancy/capacity/location/item 등 핵심 관측 상태 일치')
96
96
  })
97
97
 
@@ -112,15 +112,15 @@ function runSimRich() {
112
112
  { id: 'fk-2', kind: 'forklift', homeLocationId: 'dock-recv' }
113
113
  ]
114
114
  }
115
- const board = masterToTwin(m).board
115
+ const model = masterToTwin(m).model
116
116
  const kernel = new WmsKernel('t')
117
117
  const events: any[] = []
118
118
  kernel.onEvent((e: any) => events.push(e))
119
- kernel.loadBoard(board)
119
+ kernel.loadTwinModel(model)
120
120
  kernel.scenario.load({ seed: 7, generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 600 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 1, max: 1 } } }] })
121
121
  kernel.scenario.start()
122
122
  for (let i = 0; i < 120; i++) kernel.tick(1000) // 이동중·고장·병목이 살아있는 시점
123
- const proj = new StateProjector(board)
123
+ const proj = new StateProjector(model)
124
124
  for (const env of events) proj.apply(env)
125
125
  return { sim: kernel.getSnapshot() as any, live: proj.snapshot() as any }
126
126
  }
@@ -195,10 +195,17 @@ test('★ OEE derive: 공유 공식 computeOee 가 공급된 카운터로 OEE
195
195
  assert.ok(m?.oee, 'sim 무버 oee 존재')
196
196
  const o = m.oee
197
197
 
198
- // live 텔레메트리/누적기가 공급할 카운터 + 재구성 nowMs(planned) 로 공식 재적용.
198
+ /*
199
+ * live 텔레메트리/누적기가 공급할 카운터 + **계측 창** 으로 공식 재적용.
200
+ *
201
+ * 창을 명시한다(`metricsSinceMs: 0` 부터 `planned` 까지). 예전 이 검사는 창을 넘기지 않고 공식이
202
+ * 그것을 epoch(0)로 메우는 것에 **기대고** 있었다 — 그 메움이 화면에 56,000년 유휴를 세운 결함이라
203
+ * 없앴다. 이제 창을 모르면 잰 시간이 없으므로, 재생산에는 창이 함께 와야 한다. 라이브 공급자가
204
+ * 실제로 하는 일이 그것이다(누적기는 첫 이벤트 시각을 창으로 낸다).
205
+ */
199
206
  const planned = o.runMs + o.setupMs + o.downMs + o.idleMs
200
207
  const derived = computeOee(
201
- { runMs: o.runMs, setupMs: o.setupMs, downMs: o.downMs, goodCount: o.goodCount, scrapCount: o.scrapCount },
208
+ { runMs: o.runMs, setupMs: o.setupMs, downMs: o.downMs, goodCount: o.goodCount, scrapCount: o.scrapCount, metricsSinceMs: 0 },
202
209
  planned
203
210
  )
204
211
  const near = (a: number, b: number) => Math.abs(a - b) < 1e-6
@@ -231,3 +238,36 @@ test('★ withLiveAttentions 는 채널을 하나도 떨어뜨리지 않는다(
231
238
  for (const expected of ['breakdown', 'bottleneck', 'hold', 'work-backlog'])
232
239
  assert.ok(kinds.has(expected), `라이브 배선이 '${expected}' 채널을 떨어뜨렸다 — 넘긴 것: ${[...kinds].join(', ') || '없음'}`)
233
240
  })
241
+
242
+ /*
243
+ * ── 되짚은 상태에도 신호가 있어야 한다 (2026-08-05) ────────────────────────
244
+ * 라이브·시뮬은 커널이 신호를 스스로 내지만, 저널을 접는 경로는 프로젝터 상태만 낸다 — 신호 키가
245
+ * 아예 없었다. 지도는 `snap.attentions` 를 읽으므로 **과거를 되짚으면 주의 레일이 텅 비었고**,
246
+ * 기동돼 있지 않은 트윈을 보는 화면도 같았다. 실 저널로 재 보니 `rosarito-wms` 는 246건이 비어 있었다.
247
+ *
248
+ * 신호는 상태에서 계산되는 것이므로 같은 공식으로 채우면 된다(두 벌을 두지 않는다). 다만 "늦었나"
249
+ * 판정이 `nowTime` 을 보므로 그 값이 먼저다 — 되짚은 시점의 정직한 "지금" 은 물어본 시각이고,
250
+ * 벽시계를 끼워 넣으면 과거 화면이 "지금 기준으로 늦었다" 고 말한다.
251
+ */
252
+ test('★ 프로젝터 폴드 상태에는 신호 키가 없다 — 보강이 필요한 이유', () => {
253
+ const { model, events } = runSim()
254
+ const proj = new StateProjector(model)
255
+ for (const env of events) proj.apply(env)
256
+ const raw: any = proj.snapshot()
257
+ assert.equal(raw.attentions, undefined, '접은 상태는 신호를 스스로 내지 않는다(커널이 아니다)')
258
+ })
259
+
260
+ test('★ 보강하면 다시 계산한 상태에도 신호가 있다 — 지도의 주의 레일이 비지 않게', () => {
261
+ /* 용량 1 자리를 채워 병목이 스스로 성립하게 만든다(신호를 손으로 넣지 않는다). */
262
+ const st: any = {
263
+ revision: 3,
264
+ nowTime: '2026-08-05T00:00:00.000Z',
265
+ locations: [{ id: 'bin-tight', type: 'storage', capacity: 1, occupancy: 1 }],
266
+ equipment: [],
267
+ orders: [],
268
+ tasks: []
269
+ }
270
+ const out = withLiveAttentions(st)
271
+ assert.ok((out.attentions ?? []).length > 0, '접은 상태에서도 규칙이 신호를 낸다')
272
+ assert.equal(out.attentions![0].kind, 'bottleneck')
273
+ })