@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
@@ -1,6 +1,6 @@
1
1
  /*
2
- * masterToTwin — 모델→board 토폴로지 변환 시임. 템플릿/레퍼런스 master → board{locations,equipment,layout,spaceId}+space.
3
- * 스트림라인의 "모델링→board" 연결부: 템플릿(build)이 낸 master 를 커널이 소비할 BoardDef 로 바꾼다.
2
+ * masterToTwin — 모델→model 토폴로지 변환 시임. 템플릿/레퍼런스 master → model{locations,equipment,layout,spaceId}+space.
3
+ * 스트림라인의 "모델링→board" 연결부: 템플릿(build)이 낸 master 를 커널이 소비할 TwinModelDef 로 바꾼다.
4
4
  * role(location/area/both) 분기·footprint→layout·spaceId 파생·미지 타입 경고를 고정.
5
5
  */
6
6
  import { test } from 'node:test'
@@ -26,41 +26,45 @@ const wmsMaster = () => ({
26
26
  })
27
27
 
28
28
  test('locations role → 노드/영역 분기, equipment → 무버', () => {
29
- const { board, spaceContent } = masterToTwin(wmsMaster())
30
- assert.deepEqual(board.locations.map((n: any) => n.id).sort(), ['dock-recv', 'dock-ship', 'rack-1', 'rack-2'], '노드=role location')
29
+ const { model, spaceContent } = masterToTwin(wmsMaster())
30
+ assert.deepEqual(model.locations.map((n: any) => n.id).sort(), ['dock-recv', 'dock-ship', 'rack-1', 'rack-2'], '노드=role location')
31
31
  assert.equal(spaceContent.areas.length, 1, '영역=role area')
32
32
  assert.equal(spaceContent.areas[0].id, 'a-dc')
33
- assert.deepEqual(board.equipment, [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock-recv', mtbfMs: undefined, mttrMs: undefined, gs1Id: undefined }], '마스터 equipment→보드 equipment(homeLocationId→homeLocation, gs1Id 통과)')
34
- assert.equal(board.locations.find((n: any) => n.id === 'rack-1').capacity, 60, 'capacity 전달')
35
- assert.equal(board.locations.find((n: any) => n.id === 'dock-recv').parentId, undefined, 'parentId 없으면 undefined')
33
+ assert.deepEqual(model.equipment, [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock-recv', mtbfMs: undefined, mttrMs: undefined, gs1Id: undefined }], '마스터 equipment→보드 equipment(homeLocationId→homeLocation, gs1Id 통과)')
34
+ assert.equal(model.locations.find((n: any) => n.id === 'rack-1').capacity, 60, 'capacity 전달')
35
+ assert.equal(model.locations.find((n: any) => n.id === 'dock-recv').parentId, undefined, 'parentId 없으면 undefined')
36
36
  })
37
37
 
38
38
  test('coordinate-free: footprint 없으면 layout 비어있음(미배치 → A2)', () => {
39
- const { board } = masterToTwin(wmsMaster())
40
- assert.deepEqual(board.layout, {}, '좌표 없는 master → layout {}')
39
+ const { model } = masterToTwin(wmsMaster())
40
+ assert.deepEqual(model.layout, {}, '좌표 없는 master → layout {}')
41
41
  })
42
42
 
43
43
  test('footprint 있으면 layout = 중심좌표', () => {
44
44
  const m = wmsMaster()
45
45
  ;(m.locations[1] as any).footprint = { x: 10, y: 20, w: 4, h: 6 } // dock-recv
46
- const { board } = masterToTwin(m)
47
- assert.deepEqual(board.layout['dock-recv'], { x: 12, y: 23 }, '중심 = x+w/2, y+h/2')
48
- assert.equal(board.layout['rack-1'], undefined, 'footprint 없는 노드는 layout 없음')
46
+ const { model } = masterToTwin(m)
47
+ assert.deepEqual(model.layout['dock-recv'], { x: 12, y: 23 }, '중심 = x+w/2, y+h/2')
48
+ assert.equal(model.layout['rack-1'], undefined, 'footprint 없는 노드는 layout 없음')
49
49
  })
50
50
 
51
51
  test('spaceId: 명시 시 참조, 없으면 source 파생 (공동배치 시임)', () => {
52
- assert.equal(masterToTwin(wmsMaster()).board.spaceId, 'wh-seoul-site', '없으면 {source}-site')
52
+ assert.equal(masterToTwin(wmsMaster()).model.spaceId, 'wh-seoul-site', '없으면 {source}-site')
53
53
  const shared = wmsMaster()
54
54
  ;(shared.space as any).spaceId = 'rosarito-site'
55
- assert.equal(masterToTwin(shared).board.spaceId, 'rosarito-site', '명시 시 공유(다시스템 co-location)')
55
+ assert.equal(masterToTwin(shared).model.spaceId, 'rosarito-site', '명시 시 공유(다시스템 co-location)')
56
56
  })
57
57
 
58
58
  test('미지 노드 타입 경고 — 카탈로그 있으면 검증(버리진 않음)', () => {
59
59
  const m = wmsMaster()
60
60
  ;(m.locations as any).push({ id: 'weird', type: '__unknown__', role: 'location', capacity: 1 })
61
61
  const withCat = masterToTwin(m, DOMAIN_CATALOG)
62
- assert.ok(withCat.warnings.some(w => w.includes('__unknown__')), '미지 타입 경고')
63
- assert.ok(withCat.board.locations.some((n: any) => n.id === 'weird'), '경고해도 노드는 유지(반영 우선)')
62
+ /* 경고는 프로즈가 아니라 **코드+파라미터**다(화면이 다섯 언어로 렌더한다) 문구가 아니라 계약을 본다. */
63
+ assert.ok(
64
+ withCat.warnings.some(w => w.code === 'unknown-location-type' && w.params?.type === '__unknown__'),
65
+ `미지 타입 경고: ${JSON.stringify(withCat.warnings)}`
66
+ )
67
+ assert.ok(withCat.model.locations.some((n: any) => n.id === 'weird'), '경고해도 노드는 유지(반영 우선)')
64
68
  // 정상 타입만이면 경고 0
65
69
  assert.equal(masterToTwin(wmsMaster(), DOMAIN_CATALOG).warnings.length, 0, '알려진 타입만 → 경고 0')
66
70
  // 카탈로그 없으면 검증 생략
@@ -70,8 +74,8 @@ test('미지 노드 타입 경고 — 카탈로그 있으면 검증(버리진
70
74
  test("role 'both' 는 노드·영역 양쪽에 등장(도크 도어 등)", () => {
71
75
  const m = wmsMaster()
72
76
  ;(m.locations as any).push({ id: 'dock-door-1', type: 'dock-door', role: 'both', name: '도어1', capacity: 1 })
73
- const { board, spaceContent } = masterToTwin(m)
74
- assert.ok(board.locations.some((n: any) => n.id === 'dock-door-1'), 'both → 노드')
77
+ const { model, spaceContent } = masterToTwin(m)
78
+ assert.ok(model.locations.some((n: any) => n.id === 'dock-door-1'), 'both → 노드')
75
79
  assert.ok(spaceContent.areas.some((a: any) => a.id === 'dock-door-1'), 'both → 영역')
76
80
  })
77
81
 
@@ -107,7 +111,7 @@ test('현장 물질화(#4): 표현도 geo 도 없으면 빈 표현 + 경고(정
107
111
  const { spaceContent, warnings } = masterToTwin(m)
108
112
  assert.equal(spaceContent.representations.length, 0, '합성 불가')
109
113
  assert.ok(
110
- warnings.some((w: string) => w.includes('no viewable representation')),
114
+ warnings.some(w => w.code === 'no-viewable-representation'),
111
115
  '뷰 없는 공간 경고 표면화'
112
116
  )
113
117
  })
@@ -115,7 +119,7 @@ test('현장 물질화(#4): 표현도 geo 도 없으면 빈 표현 + 경고(정
115
119
  /* ── 공정 명세 통과 (ISA-95 OperationsSegment) ──────────────────────────────
116
120
  *
117
121
  * 시뮬레이션의 소요·수율이 커널 상수였던 것을 데이터로 옮기는 경로의 마지막 한 칸: 마스터가 명세를
118
- * 말하면 board 를 통해 커널까지 **손대지 않고** 간다. 기간은 표준 표기(ISO 8601)로 그대로 흐른다 —
122
+ * 말하면 model 를 통해 커널까지 **손대지 않고** 간다. 기간은 표준 표기(ISO 8601)로 그대로 흐른다 —
119
123
  * 여기서 밀리초로 바꾸면 표준과 어긋나고, 커널이 다시 해석할 기회를 잃는다.
120
124
  */
121
125
  const mesMaster = () => ({
@@ -138,19 +142,19 @@ const mesMaster = () => ({
138
142
  ]
139
143
  })
140
144
 
141
- test('공정 명세가 board 로 그대로 통과한다 — 표준 표기 유지, 값 변형 없음', () => {
142
- const { board, warnings } = masterToTwin(mesMaster(), DOMAIN_CATALOG)
143
- assert.equal((board as any).operations.length, 1)
144
- const op = (board as any).operations[0]
145
+ test('공정 명세가 model 로 그대로 통과한다 — 표준 표기 유지, 값 변형 없음', () => {
146
+ const { model, warnings } = masterToTwin(mesMaster(), DOMAIN_CATALOG)
147
+ assert.equal((model as any).operations.length, 1)
148
+ const op = (model as any).operations[0]
145
149
  assert.equal(op.duration, 'PT25M', 'ISO 8601 기간을 밀리초로 바꾸지 않는다')
146
150
  assert.deepEqual(op.variability, { distribution: 'triangular', min: 'PT20M', max: 'PT35M', mode: 'PT24M' })
147
151
  assert.deepEqual(op.parameters, [{ id: 'yield', value: '0.995' }])
148
152
  assert.equal(warnings.length, 0)
149
153
  })
150
154
 
151
- test('명세를 주지 않으면 키를 만들지 않는다 — 빈 배열로 "선언했다" 말하지 않는다', () => {
152
- const { board } = masterToTwin(wmsMaster(), DOMAIN_CATALOG)
153
- assert.equal('operations' in (board as any), false)
155
+ test('명세를 전달하지 않으면 키를 만들지 않는다 — 빈 배열을 "선언했다" 보고하지 않는다', () => {
156
+ const { model } = masterToTwin(wmsMaster(), DOMAIN_CATALOG)
157
+ assert.equal('operations' in (model as any), false)
154
158
  })
155
159
 
156
160
  test('내용 없는 명세는 경고한다 — 선언했는데 아무 말도 하지 않는 명세는 침묵하지 않는다', () => {
@@ -158,7 +162,7 @@ test('내용 없는 명세는 경고한다 — 선언했는데 아무 말도 하
158
162
  m.operations = [{ key: 'cut', label: 'cutting', intent: 'process' }]
159
163
  const { warnings } = masterToTwin(m, DOMAIN_CATALOG)
160
164
  assert.ok(
161
- warnings.some(w => w.includes('cut') && w.includes('kernel defaults')),
165
+ warnings.some(w => w.code === 'operation-without-duration' && w.params?.operation === 'cut'),
162
166
  `기본값으로 굴러갈 것을 알린다: ${JSON.stringify(warnings)}`
163
167
  )
164
168
  })
@@ -168,20 +172,108 @@ test('내용 없는 명세는 경고한다 — 선언했는데 아무 말도 하
168
172
  * `capacity` 한 필드가 저장 용량과 처리 능력을 겸하던 것을 갈랐다. 인제스트도 두 축을 따로 실어야
169
173
  * 커널이 "20개를 쌓아 두지만 한 번에 한 대만 깎는다" 를 표현할 수 있다. 교대도 같다.
170
174
  */
171
- test('처리 능력과 교대가 board 로 통과한다 — 저장 용량과 섞이지 않는다', () => {
175
+ test('처리 능력과 교대가 model 로 통과한다 — 저장 용량과 섞이지 않는다', () => {
172
176
  const m: any = mesMaster()
173
177
  m.locations[1].parallelism = 1 // cut-1: 저장 4, 동시 처리 1
174
178
  m.equipment[0].window = { startHour: 8, endHour: 18 }
175
- const { board } = masterToTwin(m, DOMAIN_CATALOG)
176
- const location = (board as any).locations.find((n: any) => n.id === 'cut-1')
179
+ const { model } = masterToTwin(m, DOMAIN_CATALOG)
180
+ const location = (model as any).locations.find((n: any) => n.id === 'cut-1')
177
181
  assert.equal(location.capacity, 4, '저장 용량')
178
182
  assert.equal(location.parallelism, 1, '처리 능력 — 다른 축')
179
- assert.deepEqual((board as any).equipment[0].window, { startHour: 8, endHour: 18 })
183
+ assert.deepEqual((model as any).equipment[0].window, { startHour: 8, endHour: 18 })
180
184
  })
181
185
 
182
- test('선언하지 않으면 키를 만들지 않는다 — 미지정과 0 을 뭉개지 않는다', () => {
183
- const { board } = masterToTwin(mesMaster(), DOMAIN_CATALOG)
184
- const location = (board as any).locations.find((n: any) => n.id === 'cut-1')
186
+ test('선언하지 않으면 키를 만들지 않는다 — 미지정과 0 을 같게 다루지 않는다', () => {
187
+ const { model } = masterToTwin(mesMaster(), DOMAIN_CATALOG)
188
+ const location = (model as any).locations.find((n: any) => n.id === 'cut-1')
185
189
  assert.equal('parallelism' in location, false)
186
- assert.equal('window' in (board as any).equipment[0], false)
190
+ assert.equal('window' in (model as any).equipment[0], false)
191
+ })
192
+
193
+ /*
194
+ * ★ **이름이 첫 문에서 사라지고 있었다.**
195
+ *
196
+ * 인제스트가 자리·설비·사람·자산을 보드로 옮길 때 `name` 을 빼고 있었다. 그러면 모델에도 없고,
197
+ * 모델에서 그리는 투영 행에도 없으므로 **어떤 화면도 이름을 만들 수 없다** — 목록·표·관계 그림이
198
+ * 전부 `cut-station` 같은 id 를 찍었다. 표면마다 따로 나타나지만 결함은 이 한 자리다.
199
+ *
200
+ * 업무키를 따로 두는 설계(식별자는 원본이 정한다)에서는 **이름을 함께 싣는 것이 그 설계의 절반**이다.
201
+ * 나누기만 하고 이름을 빼면 화면에 남는 것은 id 뿐이다.
202
+ */
203
+ test('이름이 보드로 통과한다 — 업무키를 나눈 설계의 나머지 절반', () => {
204
+ const m: any = mesMaster()
205
+ m.locations[1].name = '프레임 절단'
206
+ m.equipment[0].name = '절단기 1호'
207
+ const { model } = masterToTwin(m, DOMAIN_CATALOG)
208
+ const location = (model as any).locations.find((n: any) => n.id === 'cut-1')
209
+ assert.equal(location.name, '프레임 절단', '자리 이름이 모델에 실려야 한다')
210
+ assert.equal((model as any).equipment[0].name, '절단기 1호', '설비 이름이 모델에 실려야 한다')
211
+ })
212
+
213
+ test('이름 없는 원본은 이름 없이 통과한다 — id 를 이름으로 베끼지 않는다', () => {
214
+ const m: any = mesMaster()
215
+ delete m.locations[1].name // 이름을 말하지 않는 원본
216
+ const { model } = masterToTwin(m, DOMAIN_CATALOG)
217
+ const location = (model as any).locations.find((n: any) => n.id === 'cut-1')
218
+ /* 키 자체를 만들지 않는다: id 를 넣어 두면 "이름이 있다" 가 거짓이 되고, 화면은 이름 없음을
219
+ 알아볼 수 없게 된다(그러면 "이름을 지어 주세요" 라고 말할 수도 없다). */
220
+ assert.equal('name' in location, false, '원본이 이름을 말하지 않았으면 키가 없어야 한다')
221
+ })
222
+
223
+ /*
224
+ * ★ 검사 명세가 모델로 통과한다 — **참조가 가리킬 대상이 실린다.**
225
+ *
226
+ * 자원은 예전부터 `testSpecificationIds` 로 시험을 가리킬 수 있었지만 가리켜지는 대상이 없었다. 인제스트가
227
+ * 명세를 흘리면 축이 서 있어도 여전히 허공을 가리킨다 — 그래서 이 문에서 통과를 못 박는다.
228
+ *
229
+ * 표시명은 `name` 으로 옮긴다(등급과 같은 규칙) — 그러지 않으면 화면의 "설명" 열에 이름이 앉는다.
230
+ */
231
+ test('검사 명세가 모델로 통과한다 — 표시명은 name 으로', () => {
232
+ const m: any = mesMaster()
233
+ m.testSpecifications = [{ id: 'weld-cert', description: 'twin.x.test.weld', version: '2026.1' }]
234
+ const { model } = masterToTwin(m, DOMAIN_CATALOG) as any
235
+ const specs = (model as any).testSpecifications
236
+ assert.equal(specs?.length, 1, '명세가 모델에 실려야 한다')
237
+ assert.equal(specs[0].id, 'weld-cert')
238
+ assert.equal(specs[0].name, 'twin.x.test.weld', '표시명은 name 자리에 온다(어휘를 하나로)')
239
+ assert.equal('description' in specs[0], false, '두 자리에 같은 값을 두지 않는다')
240
+ assert.equal(specs[0].version, '2026.1', '판이 남아야 한다 — 어느 판으로 검증했나')
241
+ })
242
+
243
+ test('선언하지 않으면 키를 만들지 않는다 — 빈 배열을 "선언했다" 로 보고하지 않는다', () => {
244
+ const { model } = masterToTwin(mesMaster(), DOMAIN_CATALOG) as any
245
+ assert.equal('testSpecifications' in (model as any), false)
246
+ })
247
+
248
+ /*
249
+ * ── 자리의 속성이 커널까지 간다 (2026-08-14, EMS 4단계) ─────────────────────
250
+ *
251
+ * 에너지 커널은 계약전력을 **자리 속성**(`contract.kW`)에서 읽고, 없으면 계약 대비 판정을 아예
252
+ * 하지 않는다. 그래서 이 통과가 끊기면 결과가 「오류 없이 판정만 사라지는」 모양이 된다 — 화면에는
253
+ * 트윈이 정상으로 보이고 피크 경고만 영원히 오지 않는다. 눈으로는 보이지 않는 부류다.
254
+ *
255
+ * 실제로 처음에는 끊겨 있었다(타입만 열고 변환을 잇지 않았다).
256
+ */
257
+ test('자리의 속성을 모델로 통과시킨다 — 계약전력이 커널에 닿는 유일한 길', () => {
258
+ const m = {
259
+ source: 'ems-1',
260
+ system: 'ems' as const,
261
+ siteName: '공장',
262
+ space: { width: 10, depth: 10 },
263
+ locations: [
264
+ {
265
+ id: 'incoming-1',
266
+ type: 'incoming',
267
+ role: 'location' as const,
268
+ capacity: 0,
269
+ properties: [{ id: 'contract.kW', value: '4000', dataType: 'xs:double', uom: 'KWT' }]
270
+ },
271
+ { id: 'feeder-1', type: 'feeder', role: 'location' as const, capacity: 0 }
272
+ ]
273
+ }
274
+ const { model } = masterToTwin(m as any)
275
+ const incoming = model.locations.find((l: any) => l.id === 'incoming-1') as any
276
+ assert.deepEqual(incoming.properties, m.locations[0].properties, '손대지 않고 나른다')
277
+ const feeder = model.locations.find((l: any) => l.id === 'feeder-1') as any
278
+ assert.equal('properties' in feeder, false, '없는 것을 빈 배열로 만들지 않는다(모름과 없음을 섞지 않는다)')
187
279
  })
@@ -11,7 +11,7 @@
11
11
  import { test } from 'node:test'
12
12
  import assert from 'node:assert/strict'
13
13
 
14
- import { BASIS_TOLERANCE, describeModelBasis, modelBasisChanged } from '../server/engine/model-basis.ts'
14
+ import { BASIS_TOLERANCE, describeModelBasis, modelBasisChanged, toleranceOf } from '../server/engine/model-basis.ts'
15
15
 
16
16
  test('같은 근거면 바뀌지 않았다 — 키 순서에 흔들리지 않는다', () => {
17
17
  assert.equal(
@@ -84,3 +84,58 @@ test('사람이 읽는 요약을 낸다 — 로그에서 왜 달라졌는지 눈
84
84
  assert.equal(describeModelBasis({}), 'kernel defaults only')
85
85
  assert.equal(describeModelBasis(undefined), 'unknown')
86
86
  })
87
+
88
+ /*
89
+ * ★ 허용오차는 **그 공정이 선언한 변동폭에서 나온다.**
90
+ *
91
+ * 단일 10% 잣대는 두 방향으로 틀렸다. 우리 트레일러 템플릿의 용접은 변동폭을 `PT55M~PT1H35M`
92
+ * (선언 소요 `PT1H10M` 기준 −21%~+36%)로 스스로 선언한다 — 10% 관문은 그 공정의 **정상 변동조차**
93
+ * "다른 모델" 로 판정해 보정을 끊는다. 반대로 1% 변동도 치명적인 공정에는 10% 가 너무 느슨하다.
94
+ *
95
+ * 도메인 설정을 새로 만들지 않는다: 얼마나 흔들리는 것이 정상인지는 **현장이 이미 선언했다.**
96
+ */
97
+ const weld = (durationMs: number) => ({
98
+ measured: { weld: durationMs },
99
+ declared: [{ key: 'weld', duration: 'PT1H10M', variability: { distribution: 'triangular', min: 'PT55M', max: 'PT1H35M', mode: 'PT1H5M' } }]
100
+ })
101
+
102
+ test('선언된 변동폭 안의 흔들림은 같은 모델이다 — 10% 잣대는 정상 변동을 끊었다', () => {
103
+ const base = 70 * 60_000 // PT1H10M
104
+ /* 반폭 = (95분 − 55분)/2 / 70분 ≈ 28.6%. 20% 흔들림은 그 안이다 — 예전 잣대(10%)로는 끊겼다. */
105
+ assert.equal(modelBasisChanged(weld(base), weld(base * 1.2)), false, '정상 변동으로 학습이 무효화되면 안 된다')
106
+ /* 변동폭이 실제로 허용오차로 읽히는지 — 반폭 ≈ 28.6%. 이 값이 안 나오면 위 단언이 우연히 통과한다. */
107
+ const tol = toleranceOf(weld(base).declared).weld
108
+ assert.ok(tol > 0.28 && tol < 0.29, `선언에서 읽은 허용오차가 ${tol} 다`)
109
+ })
110
+
111
+ test('선언된 변동폭을 넘으면 다른 모델이다 — 넓혔다고 아무 변화나 삼키지 않는다', () => {
112
+ const base = 70 * 60_000
113
+ assert.equal(modelBasisChanged(weld(base), weld(base * 1.6)), true, '60% 변화는 정상 변동이 아니다')
114
+ })
115
+
116
+ test('정밀 공정은 더 좁게 판정한다 — 1% 변동이 치명적인 현장이 있다', () => {
117
+ const precise = (ms: number) => ({
118
+ measured: { grind: ms },
119
+ declared: [{ key: 'grind', duration: 'PT10M', variability: { distribution: 'uniform', min: 'PT9M54S', max: 'PT10M6S' } }]
120
+ })
121
+ const base = 10 * 60_000
122
+ /* 반폭 = 6초/10분 = 1%. 5% 흔들림은 이 공정에서 다른 모델이다 — 단일 10% 잣대는 삼켰다. */
123
+ assert.equal(modelBasisChanged(precise(base), precise(base * 1.05)), true, '느슨한 잣대가 정밀 공정의 변화를 삼켰다')
124
+ assert.equal(modelBasisChanged(precise(base), precise(base * 1.005)), false, '선언한 변동폭 안은 같은 모델이다')
125
+ })
126
+
127
+ test('선언이 침묵하면 기본값으로 떨어진다 — 없는 것을 지어내지 않는다', () => {
128
+ const quiet = (ms: number) => ({ measured: { pack: ms }, declared: [{ key: 'pack', duration: 'PT5M' }] })
129
+ const base = 5 * 60_000
130
+ assert.equal(modelBasisChanged(quiet(base), quiet(base * 1.05)), false, '기본 10% 안')
131
+ assert.equal(modelBasisChanged(quiet(base), quiet(base * 1.3)), true, '기본 10% 밖')
132
+ })
133
+
134
+ test('변동폭이 바뀌면 다른 모델이다 — 판정의 잣대가 조용히 바뀌지 않게', () => {
135
+ const a = weld(70 * 60_000)
136
+ const b = {
137
+ measured: { weld: 70 * 60_000 },
138
+ declared: [{ key: 'weld', duration: 'PT1H10M', variability: { distribution: 'triangular', min: 'PT30M', max: 'PT3H', mode: 'PT1H5M' } }]
139
+ }
140
+ assert.equal(modelBasisChanged(a, b), true, '변동폭 선언이 바뀌었는데 같은 모델이라고 하면 잣대가 조용히 바뀐다')
141
+ })
@@ -0,0 +1,114 @@
1
+ /*
2
+ * 저장된 모델의 어휘 세대 흡수 — **빈 공장으로 읽히지 않게.**
3
+ *
4
+ * ── 계기 (2026-08-06, 사용자 신고) ───────────────────────────────────────────
5
+ * 모델러의 엔티티 목록이 통째로 비어 있었다. **오류는 없었다.** 저장된 모델가 옛 어휘(`nodes`/`movers`)
6
+ * 인데 화면과 호스트가 새 어휘를 읽었기 때문이다. 같은 원인이 구조 서명(`N[]M[]` — 서로 다른 두 공장이
7
+ * 같아 보인다)·주행 추정기·설비 동기까지 조용히 망가뜨리고 있었다.
8
+ *
9
+ * 그래서 **DB에서 읽는 순간** 한 번 정규화한다. 이 테스트가 지키는 것은 그 정규화가 ① 세 세대를 다
10
+ * 흡수하고 ② 나머지 속성을 하나도 잃지 않고 ③ 새 모델에는 아무 일도 하지 않는 것이다.
11
+ */
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+
15
+ import { readFileSync } from 'node:fs'
16
+
17
+ import { modelColumnTransformer, canonicalModel } from '../server/engine/model-vocabulary.ts'
18
+
19
+ const LOC = [{ id: 'dock', type: 'dock', capacity: 10 }]
20
+ const EQ = [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock' }]
21
+
22
+ test('가장 오래된 세대(nodes·movers)를 흡수한다 — 사용자가 본 증상의 원인', () => {
23
+ const out = canonicalModel({ nodes: LOC, movers: EQ, spaceId: 'site-1' })
24
+
25
+ assert.deepEqual(out.locations, LOC)
26
+ assert.deepEqual(out.equipment, EQ)
27
+ })
28
+
29
+ test('중간 세대(equipmentList)도 흡수한다 — 세 세대가 저장돼 있다', () => {
30
+ const out = canonicalModel({ nodes: LOC, equipmentList: EQ })
31
+
32
+ assert.deepEqual(out.equipment, EQ)
33
+ })
34
+
35
+ test('옛 키를 **내보내지 않는다** — 소비처가 두 어휘를 다시 보지 않게', () => {
36
+ const out: any = canonicalModel({ nodes: LOC, movers: EQ, equipmentList: [] })
37
+
38
+ for (const k of ['nodes', 'movers', 'equipmentList']) assert.equal(k in out, false, `${k} 가 남았다`)
39
+ })
40
+
41
+ test('나머지 속성을 하나도 잃지 않는다 — 곁가지로 보이는 값이 전제다', () => {
42
+ /* 이 도메인에서 사소한 속성은 없다: 좌표·시각기준·명세는 소요시간·교대·생산의 전제다. */
43
+ const model = {
44
+ nodes: LOC,
45
+ movers: EQ,
46
+ spaceId: 'site-1',
47
+ areaId: 'zone-a',
48
+ layout: { dock: { x: 1, y: 2 } },
49
+ utcOffsetMinutes: 540,
50
+ unit: 'm',
51
+ productionSpec: { definition: { operations: [] } },
52
+ operations: [{ id: 'op-1' }]
53
+ }
54
+ const out: any = canonicalModel(model)
55
+
56
+ for (const k of ['spaceId', 'areaId', 'layout', 'utcOffsetMinutes', 'unit', 'productionSpec', 'operations']) {
57
+ assert.deepEqual(out[k], (model as any)[k], `${k} 가 사라졌다`)
58
+ }
59
+ })
60
+
61
+ test('새 어휘 모델에는 아무 일도 하지 않는다 — 멱등', () => {
62
+ const model = { locations: LOC, equipment: EQ, spaceId: 'site-1' }
63
+
64
+ const once: any = canonicalModel(model)
65
+ const twice: any = canonicalModel(once)
66
+
67
+ assert.deepEqual(once, model)
68
+ assert.deepEqual(twice, once)
69
+ })
70
+
71
+ test('컬렉션이 없으면 빈 배열이 된다 — 소비처가 `?? []` 를 다시 적지 않게', () => {
72
+ const out = canonicalModel({ spaceId: 'site-1' })
73
+
74
+ assert.deepEqual(out.locations, [])
75
+ assert.deepEqual(out.equipment, [])
76
+ })
77
+
78
+ test('모델이 아닌 것은 그대로 통과시킨다 — 모양을 짐작해 고치지 않는다', () => {
79
+ for (const v of [null, undefined, 0, '', 'board', [1, 2]]) {
80
+ assert.deepEqual(canonicalModel(v as any), v as any, JSON.stringify(v))
81
+ }
82
+ })
83
+
84
+ test('변환기는 **읽을 때만** 정규화한다 — 쓰기는 준 그대로', () => {
85
+ const old = { nodes: LOC, movers: EQ }
86
+
87
+ assert.deepEqual(modelColumnTransformer.from(old).equipment, EQ)
88
+ /* 쓰기를 손대면 "저장된 것" 과 "준 것" 이 달라져 추적이 어려워진다. 읽기가 정규화되므로 다음 저장에서
89
+ 자연히 새 어휘가 된다. */
90
+ assert.equal(modelColumnTransformer.to(old), old)
91
+ })
92
+
93
+ test('두 옛 모델이 구조적으로 구별된다 — 서명이 빈 문자열로 축약되지 않는다', () => {
94
+ /* 정규화 전에는 둘 다 `locations: undefined` 라 서명이 `N[]M[]` 로 같았다(변경 감지가 죽는다). */
95
+ const a = canonicalModel({ nodes: [{ id: 'a', type: 'dock', capacity: 1 }], movers: [] })
96
+ const b = canonicalModel({ nodes: [{ id: 'b', type: 'storage', capacity: 9 }], movers: [] })
97
+
98
+ assert.notDeepEqual(a.locations, b.locations)
99
+ assert.equal(a.locations.length, 1)
100
+ assert.equal(b.locations.length, 1)
101
+ })
102
+
103
+ test('보드를 저장하는 엔티티가 **변환기를 달고 있다** — 결선이 빠지면 순수 모듈은 그린인데 화면은 빈다', () => {
104
+ /* 이 결함이 정확히 그랬다: 리더는 커널에 있었고 목록만 그것을 거쳤다. 결선은 조용히 빠질 수 있으므로
105
+ 기계가 지킨다. 새 엔티티가 보드를 담게 되면 여기 함께 올린다. */
106
+ const owners = ['server/service/twin-instance/twin-instance.ts', 'server/service/twin-structure/twin-structure.ts']
107
+
108
+ for (const rel of owners) {
109
+ const src = readFileSync(new URL('../' + rel, import.meta.url).pathname, 'utf-8')
110
+ const boardColumn = src.split('\n').find(l => l.includes('@Column') && l.includes('transformer'))
111
+ assert.ok(boardColumn, `${rel}: model 컬럼에 transformer 가 없다`)
112
+ assert.match(boardColumn as string, /modelColumnTransformer/, `${rel}: 다른 변환기가 달려 있다`)
113
+ }
114
+ })
@@ -0,0 +1,149 @@
1
+ /*
2
+ * 기존 트윈을 다른 현장으로 가져오기 — 판정이 잃을 것을 말하는지 본다.
3
+ *
4
+ * 엔진 본체는 DB 를 물고 있어 단위테스트로 부를 수 없다. 그래서 **결정만** 순수 모듈로 떼어 놓고
5
+ * 여기서 본다(엔진은 사실을 조회해 넘기는 일만 한다 — `ingest-space` 와 같은 구조).
6
+ */
7
+ import { test } from 'node:test'
8
+ import assert from 'node:assert/strict'
9
+
10
+ import { judgeMoveTwinSpace, judgeDetachTwinSpace } from '../server/service/twin-space/move-space.ts'
11
+
12
+ /** 잃을 것이 없는 가장 단순한 이전 — 같은 시간대, 구역 참조 없음, 옛 표현 없음. */
13
+ const CLEAN = {
14
+ instanceId: 'wms-a',
15
+ fromSpaceId: 'wms-a-site',
16
+ toSpaceId: 'rosarito-site',
17
+ toSpaceExists: true,
18
+ fromTimezone: 'America/Los_Angeles',
19
+ toTimezone: 'America/Los_Angeles'
20
+ }
21
+
22
+ test('잃을 것이 없으면 경고도 없다 — 정상 경로를 시끄럽게 만들지 않는다', () => {
23
+ const j = judgeMoveTwinSpace(CLEAN)
24
+ assert.equal(j.refusal, undefined)
25
+ assert.deepEqual(j.warnings, [])
26
+ })
27
+
28
+ test('없는 현장으로는 옮기지 않는다 — 조용히 새 공간을 만들지 않는다', () => {
29
+ const j = judgeMoveTwinSpace({ ...CLEAN, toSpaceExists: false })
30
+ assert.equal(j.refusal?.code, 'space-not-found')
31
+ assert.equal(j.refusal?.params?.spaceId, 'rosarito-site')
32
+ })
33
+
34
+ test('같은 현장으로 옮기는 것은 거절한다 — 할 일이 없는데 쓴 것처럼 답하지 않는다', () => {
35
+ const j = judgeMoveTwinSpace({ ...CLEAN, toSpaceId: 'wms-a-site' })
36
+ assert.equal(j.refusal?.code, 'same-space')
37
+ })
38
+
39
+ test('없는 트윈은 거절한다', () => {
40
+ const j = judgeMoveTwinSpace({ ...CLEAN, instanceId: undefined })
41
+ assert.equal(j.refusal?.code, 'twin-not-found')
42
+ })
43
+
44
+ test('벤치 사본은 옮기지 않는다 — 무엇의 측정인지 말할 수 없게 된다', () => {
45
+ const j = judgeMoveTwinSpace({ ...CLEAN, purpose: 'bench' })
46
+ assert.equal(j.refusal?.code, 'bench-copy')
47
+ })
48
+
49
+ test('현장이 없던 트윈도 옮길 수 있다 — 옛 트윈을 고칠 길을 막지 않는다', () => {
50
+ const j = judgeMoveTwinSpace({ ...CLEAN, fromSpaceId: undefined, fromTimezone: undefined, toTimezone: undefined })
51
+ assert.equal(j.refusal, undefined)
52
+ })
53
+
54
+ test('새 공간에 없는 구역을 가리키는 로케이션은 소속이 끊긴다고 말한다', () => {
55
+ const j = judgeMoveTwinSpace({
56
+ ...CLEAN,
57
+ areaRefs: ['bldg-1', 'floor-2', undefined, 'bldg-1', ''],
58
+ toAreaIds: ['floor-2']
59
+ })
60
+ const w = j.warnings.find(x => x.code === 'areas-left-behind')
61
+ assert.ok(w, '끊기는 소속을 말해야 한다')
62
+ assert.equal(w.params?.count, 1, '중복·빈 값은 세지 않는다')
63
+ assert.equal(w.params?.areas, 'bldg-1')
64
+ assert.equal(j.refusal, undefined, '막지는 않는다 — 새 공간에서 구역을 다시 그리는 것이 정상이다')
65
+ })
66
+
67
+ test('구역이 모두 새 공간에 있으면 그 경고는 없다', () => {
68
+ const j = judgeMoveTwinSpace({ ...CLEAN, areaRefs: ['bldg-1'], toAreaIds: ['bldg-1', 'floor-2'] })
69
+ assert.equal(j.warnings.find(x => x.code === 'areas-left-behind'), undefined)
70
+ })
71
+
72
+ test('시간대가 다르면 말한다 — 같은 저널이 다른 시각으로 읽힌다', () => {
73
+ const j = judgeMoveTwinSpace({ ...CLEAN, fromTimezone: 'Asia/Seoul', toTimezone: 'America/Los_Angeles' })
74
+ const w = j.warnings.find(x => x.code === 'timezone-differs')
75
+ assert.ok(w)
76
+ assert.equal(w.params?.from, 'Asia/Seoul')
77
+ assert.equal(w.params?.to, 'America/Los_Angeles')
78
+ })
79
+
80
+ test('시간대를 선언하지 않은 공간도 다름으로 센다 — 모르는 것을 같다고 하지 않는다', () => {
81
+ const j = judgeMoveTwinSpace({ ...CLEAN, fromTimezone: 'Asia/Seoul', toTimezone: undefined })
82
+ assert.ok(j.warnings.find(x => x.code === 'timezone-differs'))
83
+ })
84
+
85
+ test('옛 공간의 표현은 남는다고 말한다 — 그 화면에서 이 트윈이 사라진다', () => {
86
+ const j = judgeMoveTwinSpace({ ...CLEAN, fromRepresentationCount: 3 })
87
+ const w = j.warnings.find(x => x.code === 'representations-left-behind')
88
+ assert.ok(w)
89
+ assert.equal(w.params?.count, 3)
90
+ assert.equal(w.params?.spaceId, 'wms-a-site')
91
+ })
92
+
93
+ test('실행 중 + 시간대 변화면 재기동 뒤에 적용된다고 말한다', () => {
94
+ const j = judgeMoveTwinSpace({ ...CLEAN, running: true, fromTimezone: 'Asia/Seoul', toTimezone: 'UTC' })
95
+ assert.ok(j.warnings.find(x => x.code === 'running'))
96
+ })
97
+
98
+ test('실행 중이지만 시간대가 같으면 말할 것이 없다 — 없는 걱정을 만들지 않는다', () => {
99
+ const j = judgeMoveTwinSpace({ ...CLEAN, running: true })
100
+ assert.equal(j.warnings.find(x => x.code === 'running'), undefined)
101
+ })
102
+
103
+ test('거절이면 경고를 함께 내지 않는다 — 일어나지 않을 일을 설명하지 않는다', () => {
104
+ const j = judgeMoveTwinSpace({
105
+ ...CLEAN,
106
+ toSpaceExists: false,
107
+ areaRefs: ['bldg-1'],
108
+ toAreaIds: [],
109
+ fromRepresentationCount: 2,
110
+ fromTimezone: 'Asia/Seoul',
111
+ toTimezone: 'UTC'
112
+ })
113
+ assert.ok(j.refusal)
114
+ assert.deepEqual(j.warnings, [])
115
+ })
116
+
117
+ /* ── 현장에서 떼기 ─────────────────────────────────────────────────────────── */
118
+
119
+ test('현장이 없는 트윈은 뗄 것이 없다 — 아무 일도 없는데 했다고 답하지 않는다', () => {
120
+ const j = judgeDetachTwinSpace({ instanceId: 'wms-a', fromSpaceId: undefined })
121
+ assert.equal(j.refusal?.code, 'no-space')
122
+ })
123
+
124
+ test('벤치 사본은 떼지 않는다 — 무엇의 측정인지 말할 수 없게 된다', () => {
125
+ const j = judgeDetachTwinSpace({ instanceId: 'bench-1', fromSpaceId: 'rosarito-site', purpose: 'bench' })
126
+ assert.equal(j.refusal?.code, 'bench-copy')
127
+ })
128
+
129
+ test('떼면 볼 무대가 없어진다고 반드시 말한다 — 사용자가 고장으로 읽지 않게', () => {
130
+ const j = judgeDetachTwinSpace({ instanceId: 'wms-a', fromSpaceId: 'rosarito-site' })
131
+ assert.equal(j.refusal, undefined)
132
+ assert.ok(j.warnings.find(w => w.code === 'no-stage'), '무대가 없어진다는 말은 언제나 있다')
133
+ })
134
+
135
+ test('옛 현장의 표현과 시간 기준을 말한다 — 남는 것을 밝힌다', () => {
136
+ const j = judgeDetachTwinSpace({
137
+ instanceId: 'wms-a',
138
+ fromSpaceId: 'rosarito-site',
139
+ fromRepresentationCount: 2,
140
+ fromTimezone: 'America/Los_Angeles'
141
+ })
142
+ assert.equal(j.warnings.find(w => w.code === 'representations-left-behind')?.params?.count, 2)
143
+ assert.equal(j.warnings.find(w => w.code === 'timebase-kept')?.params?.timezone, 'America/Los_Angeles')
144
+ })
145
+
146
+ test('시간대를 선언하지 않은 현장이면 시간 기준을 말하지 않는다 — 없는 사실을 만들지 않는다', () => {
147
+ const j = judgeDetachTwinSpace({ instanceId: 'wms-a', fromSpaceId: 'rosarito-site' })
148
+ assert.equal(j.warnings.find(w => w.code === 'timebase-kept'), undefined)
149
+ })