@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,74 @@
1
+ /*
2
+ * ★ 상태 축 고르기 — 리프레시가 통째로여서 무겁던 것.
3
+ *
4
+ * 지도는 구독 신호를 받고 상태를 다시 읽는다(신호는 가볍고 리프레시는 250ms 로 합쳐진다 — 설계대로).
5
+ * 문제는 **읽는 양**이었다: 실 저널로 재 보니 트윈 하나가 최대 710KB, 같은 공간에 셋이면 약 2MB.
6
+ * 그런데 지도가 쓰는 축은 `locations`·`attentions` 둘뿐이다.
7
+ *
8
+ * 여기서 지키는 것: 말하지 않으면 자르지 않는다(기존 호출부 무영향), 오타로 화면이 비지 않는다.
9
+ */
10
+ import { test } from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+
13
+ import { pickAxes, STATE_AXES } from '../server/engine/state-axes.ts'
14
+
15
+ const STATE = {
16
+ revision: 12,
17
+ nowTime: '2026-08-05T00:00:00.000Z',
18
+ simClockMs: 1000,
19
+ locations: [{ id: 'dock' }],
20
+ items: [{ epc: 'a' }, { epc: 'b' }],
21
+ equipment: [{ id: 'fk-1' }],
22
+ tasks: [{ id: 't1' }],
23
+ orders: [{ id: 'o1' }],
24
+ attentions: [{ id: 'bottleneck:dock' }],
25
+ acked: ['bottleneck:dock']
26
+ }
27
+
28
+ test('고른 축만 남는다 — 지도가 쓰는 둘', () => {
29
+ const { state, unknown } = pickAxes(STATE, ['locations', 'attentions'])
30
+ assert.deepEqual(unknown, [])
31
+ assert.deepEqual(Object.keys(state).sort(), ['attentions', 'locations', 'nowTime', 'revision', 'simClockMs'])
32
+ assert.equal(state.items, undefined, '쓰지 않는 축은 오지 않는다')
33
+ })
34
+
35
+ test('축을 지정하지 않으면 자르지 않는다 — 기존 호출부가 그대로 동작해야 한다', () => {
36
+ assert.equal(pickAxes(STATE, undefined).state, STATE)
37
+ assert.equal(pickAxes(STATE, []).state, STATE)
38
+ assert.equal(pickAxes(STATE, null).state, STATE)
39
+ })
40
+
41
+ test('시각과 리비전은 늘 남는다 — 무엇을 언제 받았는지 알아야 한다', () => {
42
+ const { state } = pickAxes(STATE, ['locations'])
43
+ assert.equal(state.revision, 12)
44
+ assert.equal(state.nowTime, STATE.nowTime)
45
+ })
46
+
47
+ test('모르는 축은 무시하되 그 사실을 돌려준다 — 오타로 화면이 비지 않게', () => {
48
+ const { state, unknown } = pickAxes(STATE, ['locations', 'attension'])
49
+ assert.deepEqual(unknown, ['attension'])
50
+ assert.ok(state.locations, '아는 축은 그대로 온다')
51
+ })
52
+
53
+ test('전부 오타면 자르지 않는다 — 빈 화면보다 통째가 낫다', () => {
54
+ const { state, unknown } = pickAxes(STATE, ['nope', 'alsonope'])
55
+ assert.equal(state, STATE)
56
+ assert.deepEqual(unknown, ['nope', 'alsonope'])
57
+ })
58
+
59
+ test('없는 축을 요청해도 키를 만들지 않는다 — 없는 것을 빈 배열로 위장하지 않는다', () => {
60
+ const { state } = pickAxes({ revision: 1, locations: [] }, ['locations', 'persons'])
61
+ assert.deepEqual(Object.keys(state).sort(), ['locations', 'revision'])
62
+ assert.equal('persons' in state, false)
63
+ })
64
+
65
+ test('상태가 아니면 그대로 돌려준다', () => {
66
+ assert.equal(pickAxes(null, ['locations']).state, null)
67
+ assert.equal(pickAxes(undefined, ['locations']).state, undefined)
68
+ })
69
+
70
+ test('축 목록이 상태의 축과 어긋나지 않는다 — 이름이 바뀌면 여기서 걸린다', () => {
71
+ for (const a of ['locations', 'items', 'equipment', 'attentions', 'acked']) {
72
+ assert.ok((STATE_AXES as readonly string[]).includes(a), a)
73
+ }
74
+ })
@@ -1,6 +1,6 @@
1
1
  /*
2
- * ★ 캡스톤 · 스트림라인 전 사슬(in-process) — 모델 → board → 커널 → 라이브 payload.
3
- * master(템플릿 산출 동형) → masterToTwin → WmsKernel.loadBoard → tick → getSnapshot → buildEntityDeltas.
2
+ * ★ 캡스톤 · 스트림라인 전 사슬(in-process) — 모델 → model → 커널 → 라이브 payload.
3
+ * master(템플릿 산출 동형) → masterToTwin → WmsKernel.loadTwinModel → tick → getSnapshot → buildEntityDeltas.
4
4
  * 이 한 테스트가 조각들이 "함께" 동작함을 증명(개별 단위테스트 위의 통합).
5
5
  */
6
6
  import { test } from 'node:test'
@@ -8,7 +8,7 @@ import assert from 'node:assert/strict'
8
8
 
9
9
  import { WmsKernel, TwinRuntime } from '@operato/twin-kernel'
10
10
  import { masterToTwin } from '../server/service/reference/reference-master.ts'
11
- import { buildEntityDeltas } from '../server/engine/entity-delta.ts'
11
+ import { buildEntityDeltas } from '../shared/entity-delta.ts'
12
12
 
13
13
  const wmsMaster = () => ({
14
14
  source: 'wh-e2e',
@@ -35,25 +35,25 @@ const snapshotState = (kernel: any) => {
35
35
  }
36
36
 
37
37
  test('★ 캡스톤: master → masterToTwin → 커널 로드 → tick → 라이브 payload (전 사슬 결합)', () => {
38
- // 1) 모델 → board 토폴로지
39
- const { board } = masterToTwin(wmsMaster())
40
- assert.equal(board.locations.length, 5, '노드 5')
41
- assert.equal(board.equipment.length, 2, '무버 2')
38
+ // 1) 모델 → model 토폴로지
39
+ const { model } = masterToTwin(wmsMaster())
40
+ assert.equal(model.locations.length, 5, '노드 5')
41
+ assert.equal(model.equipment.length, 2, '무버 2')
42
42
 
43
43
  // 2) 커널 로드 + 런타임 tick
44
44
  const kernel = new WmsKernel('e2e')
45
- kernel.loadBoard(board)
45
+ kernel.loadTwinModel(model)
46
46
  const runtime = new TwinRuntime(kernel)
47
47
  for (let i = 0; i < 5; i++) runtime.tick(1000)
48
48
 
49
49
  // 3) 스냅샷 → 라이브 payload(보드 컴포넌트가 구독할 data(tag))
50
50
  const state = snapshotState(kernel)
51
- const deltas = buildEntityDeltas(state)
51
+ const deltas = buildEntityDeltas(state, 't1')
52
52
  const tags = new Set(deltas.map(d => d.tag))
53
53
 
54
- // 4) 보드의 모든 노드·무버가 payload 에 도달(커널→보드 결합 성립)
55
- for (const n of board.locations) assert.ok(tags.has(n.id), `노드 ${n.id} payload 도달`)
56
- for (const m of board.equipment) assert.ok(tags.has(m.id), `무버 ${m.id} payload 도달`)
54
+ // 4) 모델의 모든 노드·무버가 payload 에 도달(커널→보드 결합 성립)
55
+ for (const n of model.locations) assert.ok(tags.has(n.id), `노드 ${n.id} payload 도달`)
56
+ for (const m of model.equipment) assert.ok(tags.has(m.id), `무버 ${m.id} payload 도달`)
57
57
 
58
58
  // 5) 능력별 상태필드가 실림 — storable 노드 occupancy/capacity, mobile+operable 무버 location/status
59
59
  const rack = deltas.find(d => d.tag === 'rack-1')!.data as any
@@ -63,14 +63,14 @@ test('★ 캡스톤: master → masterToTwin → 커널 로드 → tick → 라
63
63
  assert.ok('location' in fk && 'status' in fk, 'mobile+operable payload 필드')
64
64
  })
65
65
 
66
- test('캡스톤: 결정성 — 같은 board+tick 은 같은 payload 시그니처', () => {
66
+ test('캡스톤: 결정성 — 같은 model+tick 은 같은 payload 시그니처', () => {
67
67
  const run = () => {
68
- const { board } = masterToTwin(wmsMaster())
68
+ const { model } = masterToTwin(wmsMaster())
69
69
  const kernel = new WmsKernel('det')
70
- kernel.loadBoard(board)
70
+ kernel.loadTwinModel(model)
71
71
  const runtime = new TwinRuntime(kernel)
72
72
  for (let i = 0; i < 8; i++) runtime.tick(1000)
73
- return JSON.stringify(buildEntityDeltas(snapshotState(kernel)))
73
+ return JSON.stringify(buildEntityDeltas(snapshotState(kernel), 't1'))
74
74
  }
75
75
  assert.equal(run(), run(), '동일 입력 → 동일 payload(결정적)')
76
76
  })
@@ -51,11 +51,11 @@ const boardWith = (bins: number, forklifts: number): any => ({
51
51
  })
52
52
 
53
53
  /** 커널을 실제로 굴려 봉투를 얻는다 — 손으로 지어낸 모양은 검증이 아니다. */
54
- function realEvents(board: any, ticks: number): any[] {
54
+ function realEvents(model: any, ticks: number): any[] {
55
55
  const journal = new EventJournal()
56
56
  const k = new WmsKernel('struct')
57
57
  k.onEvent((e: any) => journal.append(e))
58
- k.loadBoard(board)
58
+ k.loadTwinModel(model)
59
59
  k.scenario.load({
60
60
  seed: 11,
61
61
  generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 120 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 1, max: 1 } } }]
@@ -89,14 +89,15 @@ beforeEach(async () => {
89
89
  for (const target of [TwinEvent, TwinStructure, TwinInstance]) await ds.query(`DELETE FROM "${ds.getMetadata(target).tableName}"`)
90
90
  /* 엔진의 리비전 캐시도 비운다 — 스펙 간에 남으면 다음 스펙이 옛 번호를 찍는다. */
91
91
  ;(TwinEngine as any).structureRevCache = {}
92
- delete (TwinEngine as any).recovered[INSTANCE]
92
+ /* 키가 `(domainId, instanceId)` 로 바뀌었다 — id 만으로 지우면 아무것도 지워지지 않는다. */
93
+ delete (TwinEngine as any).recovered[`${domainId}:${INSTANCE}`]
93
94
  })
94
95
 
95
96
  test('같은 구조로 다시 프로비저닝하면 리비전이 늘지 않는다 — 부팅마다 늘면 이력이 잘게 쪼개진다', async () => {
96
- const board = boardWith(2, 1)
97
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
98
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
99
- await TwinEngine.provision(domainId, INSTANCE, 'wms', JSON.parse(JSON.stringify(board)))
97
+ const model = boardWith(2, 1)
98
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
99
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
100
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', JSON.parse(JSON.stringify(model)))
100
101
 
101
102
  const revs = await ds.getRepository(TwinStructure).find({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
102
103
  assert.equal(revs.length, 1)
@@ -164,7 +165,7 @@ test('구조 번호가 없는 옛 행은 **가장 오래된 구조**로 접는
164
165
  assert.equal(state.equipment.length, 3, '마지막 구조가 자원의 진실이다')
165
166
  })
166
167
 
167
- test('리비전이 하나뿐이면 예전과 똑같이 접는다 — 갈아타기가 결과를 바꾸지 않는다', async () => {
168
+ test('리비전이 하나뿐이면 예전과 똑같이 접는다 — 구조 전환이 결과를 바꾸지 않는다', async () => {
168
169
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(3, 2))
169
170
  const events = realEvents(boardWith(3, 2), 300)
170
171
  await TwinEngine.persistBatch(domainId, INSTANCE, events, 0)
@@ -202,7 +203,7 @@ test('시간여행은 **그때의 공장**을 보여준다 — 과거에 최신
202
203
  assert.equal(past.locations.filter((n: any) => n.type === 'storage').length, 2, '그때는 자리도 둘이었다')
203
204
  })
204
205
 
205
- test('구조 이력이 **무엇이 달라졌는지** 말한다 — 보드 두 개를 눈으로 비교할 수는 없다', async () => {
206
+ test('구조 이력이 **무엇이 달라졌는지** 알린다 — 보드 두 개를 눈으로 비교할 수는 없다', async () => {
206
207
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
207
208
  const first = realEvents(boardWith(2, 1), 200)
208
209
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
@@ -248,12 +249,12 @@ test('지문 표기가 낡았을 뿐이면 리비전을 만들지 않는다 —
248
249
  * 바뀐 트윈들이 재기동 한 번에 전부 "구조가 바뀌었다" 로 기록됐다. 저장된 **보드**와 비교하면
249
250
  * 그런 일이 없다.
250
251
  */
251
- const board = boardWith(2, 1)
252
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
252
+ const model = boardWith(2, 1)
253
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
253
254
  const table = ds.getMetadata(TwinStructure).tableName
254
255
  await ds.query(`UPDATE "${table}" SET signature = 'old-style-signature-from-a-previous-algorithm'`)
255
256
 
256
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
257
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
257
258
 
258
259
  const revs = await ds.getRepository(TwinStructure).find({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
259
260
  assert.equal(revs.length, 1, '공장은 그대로인데 리비전이 늘면 이력이 거짓으로 부푼다')
@@ -277,7 +278,7 @@ test('sim-world 로 다시 띄워도 저널이 살아남는다 — 선언이 지
277
278
  const kept = await ds.getRepository(TwinEvent).count({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
278
279
  assert.equal(kept, first.length, 'sim-world 는 이어지는 현실이다 — 지우면 선언이 거짓이 된다')
279
280
  } finally {
280
- await TwinEngine.stop(INSTANCE)
281
+ await TwinEngine.stop(domainId, INSTANCE)
281
282
  }
282
283
  })
283
284
 
@@ -295,7 +296,7 @@ test('★ 재기동 후 새 이벤트는 마지막 번호 뒤에 온다 — 겹
295
296
  `이어 세지 않았다 — 저널 마지막 ${first.length}, 커널 ${kernel.getSnapshot().revision}`
296
297
  )
297
298
  } finally {
298
- await TwinEngine.stop(INSTANCE)
299
+ await TwinEngine.stop(domainId, INSTANCE)
299
300
  }
300
301
  })
301
302
 
@@ -305,6 +306,6 @@ test('저널이 비어 있으면 이어 셀 것이 없다 — 새로 시작하
305
306
  try {
306
307
  assert.equal((rt.kernel as any).getSnapshot().revision, 0)
307
308
  } finally {
308
- await TwinEngine.stop(INSTANCE)
309
+ await TwinEngine.stop(domainId, INSTANCE)
309
310
  }
310
311
  })
@@ -0,0 +1,149 @@
1
+ /*
2
+ * ★ 두 테넌트가 **같은 인스턴스 id** 로 각자 돈다 — 실 DB로 경계를 넘어 본다.
3
+ *
4
+ * ── 계기 (2026-08-06 테넌트 격리 전수 확인) ──────────────────────────────────
5
+ * 런타임 레지스트리가 `instanceId` 하나로 키를 잡고 있었다. 그런데 DB 유일성은 `[domain, instanceId]`
6
+ * 이고 트윈의 정체성도 그쪽이다. **충돌은 이론이 아니다** — 레퍼런스 경로가 `instanceId` 로 레퍼런스
7
+ * 소스 이름(`sap-ewm-1710`)을 쓰고, 벤더 시스템 이름은 테넌트가 공유하는 어휘다.
8
+ *
9
+ * 그때 무엇이 틀렸나: `start`/`startFromRegistry` 가 **남의 런타임을 돌려주고**, `ingestLive` 는 도메인을
10
+ * 확인하지 않아 남의 커널에 이벤트를 적용할 수 있었고, 목록의 `running` 이 남의 런타임을 봤다.
11
+ *
12
+ * 이 결함은 **유닛으로는 안 잡힌다** — 두 테넌트가 실제로 등록되고 각자 기동해야 드러난다. 그래서
13
+ * 실 엔티티 sqlite 스키마 + 빌드 산출물로 경계를 넘는다(`structure-revision-db.test.ts` 와 같은 하네스).
14
+ * **소스를 고친 뒤에는 `npm run build:server` 를 먼저 돌려야 반영된다.**
15
+ */
16
+ import { test, before, after, afterEach } 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 { DataSource: TypeOrmDataSource } = req('typeorm')
27
+
28
+ let ds: DataSource
29
+ let alpha: string
30
+ let beta: string
31
+
32
+ /** 두 테넌트가 같은 벤더 시스템을 연결했을 때 실제로 생기는 id. */
33
+ const SHARED_ID = 'sap-ewm-1710'
34
+
35
+ const model = (): any => ({
36
+ spaceId: 'shared-space',
37
+ utcOffsetMinutes: 0,
38
+ locations: [
39
+ { id: 'dock-recv', type: 'dock', capacity: 100 },
40
+ { id: 'bin-1', type: 'storage', capacity: 20 }
41
+ ],
42
+ equipment: [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock-recv' }]
43
+ })
44
+
45
+ before(async () => {
46
+ ds = new TypeOrmDataSource({
47
+ type: 'sqlite',
48
+ database: ':memory:',
49
+ synchronize: true,
50
+ dropSchema: true,
51
+ namingStrategy: new NamingStrategy(),
52
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
53
+ logging: false
54
+ })
55
+ await ds.initialize()
56
+ addDataSource('default', ds)
57
+
58
+ const a = await ds.getRepository(Domain).save({ name: 'tenant-alpha', subdomain: 'tenant-alpha' } as any)
59
+ const b = await ds.getRepository(Domain).save({ name: 'tenant-beta', subdomain: 'tenant-beta' } as any)
60
+ alpha = a.id
61
+ beta = b.id
62
+
63
+ /* 두 테넌트가 **같은 id** 로 각자 프로비저닝한다 — DB 유일성이 `[domain, instanceId]` 라 허용된다. */
64
+ await TwinEngine.provision(alpha, SHARED_ID, 'wms', model())
65
+ await TwinEngine.provision(beta, SHARED_ID, 'wms', model())
66
+ })
67
+
68
+ /**
69
+ * **스펙마다 반드시 멈춘다.** 남기면 두 가지가 따라온다: 틱 타이머 때문에 러너가 끝나지 않고, 틱이
70
+ * 뒤늦게 던지는 DB 쓰기가 teardown 이후에 도착해 `SQLITE_MISUSE` 로 스위트를 오염시킨다.
71
+ */
72
+ afterEach(async () => {
73
+ await TwinEngine.stop(alpha, SHARED_ID).catch(() => {})
74
+ await TwinEngine.stop(beta, SHARED_ID).catch(() => {})
75
+ })
76
+
77
+ after(async () => {
78
+ /* 정지 직후에도 진행 중인 쓰기가 있을 수 있다 — 커넥션을 닫기 전에 가라앉힌다. */
79
+ await new Promise(resolve => setTimeout(resolve, 120))
80
+ await ds?.destroy()
81
+ })
82
+
83
+ test('두 테넌트가 같은 id 로 프로비저닝된다 — 서로를 막지 않는다', async () => {
84
+ /* 예전에는 한쪽이 **떠 있으면** 다른 쪽 재프로비전이 "instance is running" 으로 막혔다(존재 노출). */
85
+ const listA = await TwinEngine.list(alpha)
86
+ const listB = await TwinEngine.list(beta)
87
+
88
+ assert.equal(listA.filter((x: any) => x.instanceId === SHARED_ID).length, 1)
89
+ assert.equal(listB.filter((x: any) => x.instanceId === SHARED_ID).length, 1)
90
+ })
91
+
92
+ test('한쪽만 기동하면 다른 쪽은 **정지한 것으로** 보인다 — 이 회귀가 핵심', async () => {
93
+ await TwinEngine.startFromRegistry(alpha, SHARED_ID)
94
+
95
+ assert.equal(TwinEngine.owns(alpha, SHARED_ID), true, 'alpha 는 자기 트윈을 갖는다')
96
+ assert.equal(TwinEngine.owns(beta, SHARED_ID), false, 'beta 는 기동하지 않았다')
97
+
98
+ /* 목록의 running 이 남의 런타임을 보면 안 된다 — 예전에는 beta 도 running: true 였다. */
99
+ const rowA = (await TwinEngine.list(alpha)).find((x: any) => x.instanceId === SHARED_ID)
100
+ const rowB = (await TwinEngine.list(beta)).find((x: any) => x.instanceId === SHARED_ID)
101
+ assert.equal(rowA.running, true)
102
+ assert.equal(rowB.running, false)
103
+ })
104
+
105
+ test('둘 다 기동하면 **서로 다른 런타임**이다 — 남의 것을 돌려받지 않는다', async () => {
106
+ const rtA = await TwinEngine.startFromRegistry(alpha, SHARED_ID)
107
+ const rtB = await TwinEngine.startFromRegistry(beta, SHARED_ID)
108
+
109
+ /* 예전에는 두 번째 호출이 첫 번째의 런타임을 그대로 돌려줬다(조기 반환이 id 로만 찾았다). */
110
+ assert.notEqual(rtA, rtB, '같은 객체를 돌려주면 두 테넌트가 한 커널을 공유한다')
111
+ assert.notEqual(TwinEngine.kernel(alpha, SHARED_ID), TwinEngine.kernel(beta, SHARED_ID))
112
+ assert.equal(rtA.domainId, alpha)
113
+ assert.equal(rtB.domainId, beta)
114
+ })
115
+
116
+ test('접근자가 테넌트별로 답한다 — 커널·스냅샷·런타임', async () => {
117
+ await TwinEngine.startFromRegistry(alpha, SHARED_ID)
118
+ await TwinEngine.startFromRegistry(beta, SHARED_ID)
119
+
120
+ for (const [who, other] of [
121
+ [alpha, beta],
122
+ [beta, alpha]
123
+ ]) {
124
+ assert.ok(TwinEngine.kernel(who, SHARED_ID), '자기 커널은 있다')
125
+ assert.notEqual(TwinEngine.kernel(who, SHARED_ID), TwinEngine.kernel(other, SHARED_ID))
126
+ assert.notEqual(TwinEngine.runtime(who, SHARED_ID), TwinEngine.runtime(other, SHARED_ID))
127
+ }
128
+ })
129
+
130
+ test('한쪽을 정지해도 다른 쪽은 계속 동작한다 — 남의 런타임을 지우지 않는다', async () => {
131
+ await TwinEngine.startFromRegistry(alpha, SHARED_ID)
132
+ await TwinEngine.startFromRegistry(beta, SHARED_ID)
133
+
134
+ await TwinEngine.stop(alpha, SHARED_ID)
135
+
136
+ assert.equal(TwinEngine.owns(alpha, SHARED_ID), false, '정지한 쪽은 사라진다')
137
+ assert.equal(TwinEngine.owns(beta, SHARED_ID), true, '남의 트윈은 그대로다')
138
+ assert.ok(TwinEngine.kernel(beta, SHARED_ID), 'beta 커널이 살아 있다')
139
+ })
140
+
141
+ test('없는 테넌트로 물으면 아무것도 주지 않는다 — 추측한 id 로 새지 않는다', async () => {
142
+ await TwinEngine.startFromRegistry(alpha, SHARED_ID)
143
+ const stranger = '99999999-9999-9999-9999-999999999999'
144
+
145
+ assert.equal(TwinEngine.owns(stranger, SHARED_ID), false)
146
+ assert.equal(TwinEngine.kernel(stranger, SHARED_ID), undefined)
147
+ assert.equal(TwinEngine.runtime(stranger, SHARED_ID), undefined)
148
+ assert.equal(TwinEngine.metrics(stranger, SHARED_ID), null)
149
+ })
@@ -0,0 +1,83 @@
1
+ /*
2
+ * 커맨드 감사 기록 — 이 검사가 지키는 것은 **기록이 증명력을 갖는가** 다.
3
+ *
4
+ * 감사 기록의 가치는 "남아 있다" 가 아니라 "믿을 수 있다" 에 있다. 클라이언트가 보낸 신원을 그대로
5
+ * 적으면 위조가 자유로워 아무것도 증명하지 못하고, 거절된 시도를 안 남기면 "무엇을 하려 했는가" 가
6
+ * 사라진다. 그 둘이 이 파일의 핵심 검사다.
7
+ */
8
+ import { test } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+
11
+ import { buildCommandAudit, normalizeOrigin } from '../server/service/twin-audit/command-audit.ts'
12
+
13
+ const AT = new Date('2026-08-06T12:00:00.000Z')
14
+
15
+ test('명령 종류를 그대로 싣는다 — 무엇을 하려 했는지가 감사의 시작이다', () => {
16
+ const row = buildCommandAudit({ command: { commandId: 'c1', type: 'order.hold' }, ack: { accepted: true }, at: AT })
17
+ assert.equal(row.commandType, 'order.hold')
18
+ assert.equal(row.commandId, 'c1')
19
+ })
20
+
21
+ test('신원은 서버가 준 것만 적는다 — 커맨드에 실려 온 신원 주장은 무시한다', () => {
22
+ const row = buildCommandAudit({
23
+ /* 위조 시도: 클라이언트가 자기를 관리자라고 주장한다. */
24
+ command: { type: 'order.hold', actorId: 'admin', actorEmail: 'admin@corp', actorName: '관리자' },
25
+ ack: { accepted: true },
26
+ actor: { id: 'u-7', email: 'kim@corp', name: '김현장' },
27
+ at: AT
28
+ })
29
+ assert.equal(row.actorId, 'u-7')
30
+ assert.equal(row.actorEmail, 'kim@corp')
31
+ assert.equal(row.actorName, '김현장')
32
+ })
33
+
34
+ test('세션에 사용자가 없으면 비운다 — 모르는 것을 지어내지 않는다', () => {
35
+ const row = buildCommandAudit({ command: { type: 'order.hold' }, ack: { accepted: true }, at: AT })
36
+ assert.equal(row.actorId, undefined)
37
+ assert.equal(row.actorEmail, undefined)
38
+ })
39
+
40
+ test('커널이 거절한 시도도 남는다 — 무엇을 하려 했는지가 감사의 절반이다', () => {
41
+ const row = buildCommandAudit({
42
+ command: { type: 'equipment.repair', args: { id: 'cutter-1' } },
43
+ ack: { accepted: false, errorCode: 'not-broken' },
44
+ at: AT
45
+ })
46
+ assert.equal(row.accepted, false)
47
+ assert.equal(row.errorCode, 'not-broken')
48
+ assert.deepEqual(row.args, { id: 'cutter-1' })
49
+ })
50
+
51
+ test('출처는 아는 값만 — 모르면 unknown 이지 사람이 한 것으로 치지 않는다', () => {
52
+ assert.equal(normalizeOrigin('ai'), 'ai')
53
+ assert.equal(normalizeOrigin('user'), 'user')
54
+ assert.equal(normalizeOrigin('automation'), 'automation')
55
+ assert.equal(normalizeOrigin(undefined), 'unknown')
56
+ assert.equal(normalizeOrigin('관리자승인'), 'unknown')
57
+ })
58
+
59
+ test('근거는 문자열일 때만 싣는다 — 객체를 그대로 넣어 화면이 [object Object] 를 띄우지 않게', () => {
60
+ const ok = buildCommandAudit({ command: { type: 't', reason: '대기 15분 절감' }, ack: {}, at: AT })
61
+ assert.equal(ok.reason, '대기 15분 절감')
62
+ const bad = buildCommandAudit({ command: { type: 't', reason: { why: 'x' } }, ack: {}, at: AT })
63
+ assert.equal(bad.reason, undefined)
64
+ })
65
+
66
+ test('args 가 지나치게 크면 자르되 잘랐다고 알린다 — 조용히 버리지 않는다', () => {
67
+ const huge = { blob: 'x'.repeat(9000) }
68
+ const row = buildCommandAudit({ command: { type: 't', args: huge }, ack: {}, at: AT })
69
+ assert.equal(row.argsTruncated, true)
70
+ assert.equal(typeof row.args, 'string')
71
+ assert.ok((row.args as string).length <= 4000)
72
+ })
73
+
74
+ test('작은 args 는 자르지 않고 원형 그대로 — 나중에 따질 수 있어야 한다', () => {
75
+ const row = buildCommandAudit({ command: { type: 't', args: { id: 'a', qty: 3 } }, ack: {}, at: AT })
76
+ assert.equal(row.argsTruncated, false)
77
+ assert.deepEqual(row.args, { id: 'a', qty: 3 })
78
+ })
79
+
80
+ test('시각은 호출부가 준 서버 시각을 그대로 쓴다 — 클라이언트 시계를 믿지 않는다', () => {
81
+ const row = buildCommandAudit({ command: { type: 't', eventTime: '1999-01-01T00:00:00.000Z' }, ack: {}, at: AT })
82
+ assert.equal(row.eventTime.toISOString(), AT.toISOString())
83
+ })