@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,275 @@
1
+ /*
2
+ * ★ 개념 안의 목록과 항목 하나 — **막다른 길을 만들지 않는다.**
3
+ *
4
+ * 개념 지도가 숫자만 내면 그 숫자가 막다른 길이다. 인스펙션의 원칙은 "화면에 보이는 모든 식별자는
5
+ * 그 자신의 상세로 가는 문" 이므로, 셈 다음에 **목록**과 **항목**이 있어야 그 원칙이 성립한다.
6
+ *
7
+ * 여기서 지키는 것:
8
+ * · 어떻게 셌는지 밝힌다 — 행이면 DB 가 페이징, 문서면 서버가 통째로 읽어 잘랐다(`basis`)
9
+ * · 조용히 자르지 않는다 — 총 건수가 항상 함께 오고, 훑기가 상한에 닿으면 그렇다고 말한다
10
+ * · 모르는 축은 **거절한다** — 빈 목록으로 답하면 오타 하나가 "우리 트윈엔 그게 없다" 로 굳는다
11
+ * · 투영이 낡아도 항목은 열린다 — 행에 없으면 문서에서 찾는다
12
+ * · 이어진 것은 **양방향**이고, 선언된 관계만 따른다(선언에 없는 길은 그리지 않는다)
13
+ * · 관측이 없는 것과 견주지 못한 것을 구별한다(`observed: null`)
14
+ */
15
+ import { test, before, after } from 'node:test'
16
+ import assert from 'node:assert/strict'
17
+ import { createRequire } from 'node:module'
18
+ import type { DataSource } from 'typeorm'
19
+
20
+ const req = createRequire(import.meta.url)
21
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
22
+ const { entities: authEntities } = req('@things-factory/auth-base')
23
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
24
+ const { TwinEngine } = req('../dist-server/engine/index.js')
25
+ const { TwinModelItemQuery } = req('../dist-server/service/twin-model/twin-model-item-query.js')
26
+ const { TwinModelQuery } = req('../dist-server/service/twin-model/twin-model-query.js')
27
+ const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
28
+ const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
29
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
30
+
31
+ let ds: DataSource
32
+ let domainId: string
33
+ let q: any
34
+ let ctx: any
35
+
36
+ const INSTANCE = 'item-probe'
37
+
38
+ /** MES 마스터 — 자리·설비·공정에 더해 **문서에만 사는 축**(레시피·라우트)까지 있는 모양. */
39
+ const master = (): any => ({
40
+ source: INSTANCE,
41
+ system: 'mes',
42
+ siteName: '항목 공장',
43
+ space: { width: 80, depth: 40, unit: 'm' },
44
+ locations: [
45
+ { id: 'a-floor', type: 'production-line', role: 'area', name: '생산 플로어' },
46
+ { id: 'cut-station', type: 'cut-station', role: 'location', name: '절단', parentId: 'a-floor', capacity: 20 },
47
+ { id: 'weld-station', type: 'weld-station', role: 'location', name: '용접', parentId: 'a-floor', capacity: 20 }
48
+ ],
49
+ equipment: [
50
+ { id: 'cutter-1', kind: 'cutter', homeLocationId: 'cut-station', mtbfMs: 3600000 },
51
+ { id: 'welder-1', kind: 'welder', homeLocationId: 'weld-station' },
52
+ { id: 'welder-2', kind: 'welder', homeLocationId: 'weld-station' }
53
+ ],
54
+ operations: [
55
+ { key: 'cut', label: '절단', intent: 'process', locationType: 'cut-station', resourceType: 'cutter', duration: 'PT10M' },
56
+ { key: 'weld', label: '용접', intent: 'process', locationType: 'weld-station', resourceType: 'welder', duration: 'PT20M' }
57
+ ],
58
+ productionSpec: {
59
+ definition: {
60
+ materials: [{ id: 'steel' }, { id: 'frame' }],
61
+ routes: [{ key: 'frame-route', steps: ['cut', 'weld'] }],
62
+ recipes: [{ key: 'frame-recipe', route: 'frame-route', inputs: [{ material: 'steel' }], outputs: [{ material: 'frame' }] }],
63
+ operations: [
64
+ { key: 'cut', label: '절단', intent: 'process', locationType: 'cut-station', resourceType: 'cutter', duration: 'PT10M' },
65
+ { key: 'weld', label: '용접', intent: 'process', locationType: 'weld-station', resourceType: 'welder', duration: 'PT20M' }
66
+ ]
67
+ }
68
+ }
69
+ })
70
+
71
+ before(async () => {
72
+ ds = new TypeOrmDataSource({
73
+ type: 'sqlite',
74
+ database: ':memory:',
75
+ synchronize: true,
76
+ dropSchema: true,
77
+ namingStrategy: new NamingStrategy(),
78
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
79
+ logging: false
80
+ })
81
+ await ds.initialize()
82
+ addDataSource('default', ds)
83
+ const d = await ds.getRepository(Domain).save({ name: 'item-test', subdomain: 'item-test' } as any)
84
+ domainId = d.id
85
+ q = new TwinModelItemQuery()
86
+ ctx = { state: { domain: { id: domainId } } }
87
+
88
+ await TwinEngine.ingestMaster(domainId, master())
89
+ })
90
+
91
+ after(async () => {
92
+ await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
93
+ await ds?.destroy()
94
+ })
95
+
96
+ test('행으로 옮긴 축은 DB 가 페이징하고 그 사실을 밝힌다', async () => {
97
+ const page1 = await q.twinModelEntities(INSTANCE, 'equipment', ctx, undefined, 0, 2)
98
+ assert.equal(page1.basis, 'rows', 'DB 가 셌다는 사실이 응답에 있어야 한다')
99
+ assert.equal(page1.total, 3, '총 건수는 페이지 크기와 무관하게 사실이다')
100
+ assert.equal(page1.items.length, 2)
101
+
102
+ const page2 = await q.twinModelEntities(INSTANCE, 'equipment', ctx, undefined, 2, 2)
103
+ assert.equal(page2.items.length, 1, '이어 읽을 수 있다')
104
+ const seen = [...page1.items, ...page2.items].map((i: any) => i.id).sort()
105
+ assert.deepEqual(seen, ['cutter-1', 'welder-1', 'welder-2'], '두 페이지가 전건을 덮는다')
106
+ })
107
+
108
+ test('아직 문서에 사는 축도 열린다 — 이주 전이라고 안 보이면 안 된다', async () => {
109
+ const r = await q.twinModelEntities(INSTANCE, 'recipes', ctx)
110
+ assert.equal(r.basis, 'document', '문서를 읽어 잘랐다는 사실을 밝힌다(규모 한계가 드러나야 한다)')
111
+ assert.equal(r.total, 1)
112
+ assert.deepEqual(r.items.map((i: any) => i.id), ['frame-recipe'])
113
+ })
114
+
115
+ test('검색이 전수 조회라는 사실을 드러낸다 — 감추면 한계를 아무도 모른다', async () => {
116
+ const r = await q.twinModelEntities(INSTANCE, 'equipment', ctx, 'weld')
117
+ assert.equal(r.basis, 'rows-scanned', '아직 DB 로 내리지 않았다는 사실이 응답에 있다')
118
+ assert.equal(r.total, 2)
119
+ assert.equal(r.truncated, false, '상한에 닿지 않았으면 잘리지 않았다고 말한다')
120
+ assert.deepEqual(r.items.map((i: any) => i.id).sort(), ['welder-1', 'welder-2'])
121
+ })
122
+
123
+ test('모르는 축은 거절한다 — 빈 목록은 "없다" 로 읽힌다', async () => {
124
+ await assert.rejects(() => q.twinModelEntities(INSTANCE, 'nosuch', ctx), /unknown concept axis/)
125
+ await assert.rejects(() => q.twinModelItem(INSTANCE, 'nosuch', 'x', ctx), /unknown concept axis/)
126
+ })
127
+
128
+ test('항목 하나 — 정체는 카탈로그에서, 선언은 원본에서, 출처가 함께 온다', async () => {
129
+ const it = await q.twinModelItem(INSTANCE, 'equipment', 'cutter-1', ctx)
130
+ assert.ok(it, '있는 항목은 열려야 한다')
131
+ assert.equal(it.identity.typeRole, 'equipment', '표준 역할은 커널 카탈로그가 말한다')
132
+ assert.equal(it.declared.kind, 'cutter')
133
+ assert.equal(it.declared.mtbfMs, 3600000)
134
+ assert.equal(it.provenance.basis, 'rows')
135
+ assert.equal(it.provenance.sourceRef, INSTANCE, '어느 원본에서 왔는지가 항목에 붙어 있다')
136
+ })
137
+
138
+ test('비교하지 못했으면 관측은 null 이고, 답했으면 **근거를 알린다**', async () => {
139
+ const it = await q.twinModelItem(INSTANCE, 'equipment', 'cutter-1', ctx)
140
+ /*
141
+ * 이 픽스처에는 저널이 없다(행만 있다) — 되짚을 것이 없으므로 관측은 없다. **0 이나 빈 객체로
142
+ * 메우면 "관측했는데 아무것도 없었다" 로 읽힌다.**
143
+ *
144
+ * 예전 이 검사는 "트윈이 안 돌면 관측은 null" 이었다. 지금은 멈춘 트윈도 저널을 접어 답할 수 있고,
145
+ * 그 답이 **자기 근거를 달고 온다**(`observedBasis`). 그래서 규율을 그 자리에 맞춰 다시 적는다:
146
+ * 답이 없으면 null, 답이 있으면 그것이 라이브인지 되짚은 것인지 말한다. 근거 없는 답은 없다.
147
+ */
148
+ assert.equal(it.observed, null, '되짚을 저널이 없으면 관측은 없다')
149
+ assert.equal(it.observedBasis, undefined, '답이 없으면 근거도 말하지 않는다(빈 답에 근거를 붙이면 견준 것처럼 보인다)')
150
+ })
151
+
152
+ test('이어진 것은 양방향이고 선언된 관계만 따른다', async () => {
153
+ const loc = await q.twinModelItem(INSTANCE, 'locations', 'weld-station', ctx)
154
+
155
+ /* 들어오는 쪽 — 이 자리를 기준으로 삼는 설비 둘. 선언(`equipment.homeLocation`)이 알려 준다. */
156
+ const home = loc.links.incoming.find((l: any) => l.via === 'twin.rel.home' && l.from === 'equipment')
157
+ assert.ok(home, '나를 가리키는 관계가 보여야 한 걸음씩 걸을 수 있다')
158
+ assert.deepEqual(home.ids.sort(), ['welder-1', 'welder-2'])
159
+ assert.equal(home.total, 2)
160
+
161
+ /* 나가는 쪽 — 자리는 구역을 가리킨다. 대상이 **축 밖**(공간의 구역)이라는 사실을 그대로 낸다. */
162
+ const area = loc.links.out.find((l: any) => l.via === 'twin.rel.area')
163
+ assert.ok(area, '커널이 "밖" 이라 부른 참조도 이어진 것으로 낸다')
164
+ assert.equal(area.target.kind, 'external', '갈 곳이 다르므로 화면이 링크를 만들지 말지 알 수 있어야 한다')
165
+ })
166
+
167
+ test('문서에만 사는 항목의 관계도 선언대로 이어진다', async () => {
168
+ const recipe = await q.twinModelItem(INSTANCE, 'recipes', 'frame-recipe', ctx)
169
+ assert.equal(recipe.provenance.basis, 'document')
170
+
171
+ const route = recipe.links.out.find((l: any) => l.via === 'twin.rel.route')
172
+ assert.deepEqual(route.ids, ['frame-route'])
173
+
174
+ /* `inputs[].material` — 배열을 지나는 경로가 선언의 문법이고, 그것을 따르는 것이 선언을 존중하는 것이다. */
175
+ const input = recipe.links.out.find((l: any) => l.via === 'twin.rel.input')
176
+ assert.deepEqual(input.ids, ['steel'])
177
+ const output = recipe.links.out.find((l: any) => l.via === 'twin.rel.output')
178
+ assert.deepEqual(output.ids, ['frame'])
179
+ })
180
+
181
+ test('없는 항목은 null — 빈 껍데기를 내주지 않는다', async () => {
182
+ assert.equal(await q.twinModelItem(INSTANCE, 'equipment', 'no-such-machine', ctx), null)
183
+ })
184
+
185
+ test('내부 배관은 새어 나가지 않는다 — 사용자가 볼 것은 현장의 사실이지 우리 스키마가 아니다', async () => {
186
+ /*
187
+ * 실제로 새었다: `domainId`·`instanceRowId` 같은 내부 UUID 가 속성 패널까지 갔다.
188
+ * 손으로 제외 목록을 두면 컬럼이 늘 때마다 잊는다 — 관계와 조인 컬럼은 엔티티 메타데이터가 안다.
189
+ */
190
+ const INTERNAL = /^(id|domainId?|instanceRowId|areaRowId|homeLocationRowId|createdAt|updatedAt|creator|updater|creatorId|updaterId)$/
191
+
192
+ const list = await q.twinModelEntities(INSTANCE, 'locations', ctx)
193
+ for (const it of list.items)
194
+ for (const k of Object.keys(it.fields))
195
+ assert.ok(!INTERNAL.test(k), `목록에 내부 컬럼이 새었다: ${k}`)
196
+
197
+ const item = await q.twinModelItem(INSTANCE, 'locations', 'weld-station', ctx)
198
+ for (const k of Object.keys(item.declared))
199
+ assert.ok(!INTERNAL.test(k), `속성 패널에 내부 컬럼이 새었다: ${k}`)
200
+
201
+ /* 식별자도 두 번 보이지 않는다 — 이미 itemId 로 나간다. */
202
+ assert.ok(!('locationId' in item.declared), '항목 자신의 id 를 속성으로 또 내지 않는다')
203
+ /* 출처는 속성이 아니라 근거다. */
204
+ assert.ok(!('sourceRef' in item.declared) && !('ingestedAt' in item.declared))
205
+ assert.equal(item.provenance.sourceRef, INSTANCE)
206
+ })
207
+
208
+ test('지도의 선이 진단이 된다 — 이어진 것뿐 아니라 **끊긴 것**을 센다', async () => {
209
+ /*
210
+ * 선이 "선언되었고 양끝이 존재한다" 만 말하면 지도는 그림일 뿐이다. 관계에서 가장 중요한 정보는
211
+ * 대개 부재다 — 기준 자리가 비어 있는 설비, 없는 자리를 가리키는 설비.
212
+ */
213
+ const bad = master()
214
+ bad.equipment.push({ id: 'ghost-1', kind: 'welder', homeLocationId: 'no-such-station' }) // 끊어진 참조
215
+ bad.equipment.push({ id: 'floater-1', kind: 'welder' }) // 기준 자리 없음
216
+ await TwinEngine.ingestMaster(domainId, bad)
217
+
218
+ const sum = await new TwinModelQuery().twinModelSummary(INSTANCE, ctx)
219
+ const home = sum.edges.find((e: any) => e.from === 'equipment' && e.to === 'locations' && e.via === 'twin.rel.home')
220
+ assert.ok(home, '설비→자리 선이 있어야 한다')
221
+ assert.equal(home.linked, 3, '제대로 이어진 설비 셋')
222
+ assert.equal(home.dangling, 1, '없는 자리를 가리키는 설비 하나 — 가장 나쁜 부류이므로 따로 센다')
223
+ assert.equal(home.missing, 1, '기준 자리를 말하지 않은 설비 하나')
224
+
225
+ /* 선택 관계인지도 함께 온다 — `missing` 이 결손인지 정상인지는 그것으로 갈린다. */
226
+ assert.equal(home.optional, true)
227
+ })
228
+
229
+ /*
230
+ * ★ 관측 열이 **자기 근거를 말한다** — 라이브인지 되짚은 것인지.
231
+ *
232
+ * 둘은 같은 모양의 값이라, 근거가 없으면 되짚은 값을 "지금 이렇다" 로 읽는다. 그래서 근거를 실을
233
+ * 자리가 없던 동안에는 멈춘 트윈의 저널을 아예 접지 않았다(관측 열이 통째로 `—`). 자리를 만든 뒤의
234
+ * 규율은 하나다: **답하면 근거를 말한다.**
235
+ */
236
+ test('멈춘 트윈도 저널을 접어 답하고, 그 답은 다시 계산한 값이라고 알린다', async () => {
237
+ const { WmsKernel, EventJournal } = req('@operato/twin-kernel')
238
+ const model = {
239
+ spaceId: 'journal-space',
240
+ utcOffsetMinutes: 0,
241
+ locations: [
242
+ { id: 'dock-recv', type: 'dock', capacity: 100 },
243
+ { id: 'bin-1', type: 'storage', capacity: 20 }
244
+ ],
245
+ equipment: [{ id: 'fk-1', kind: 'forklift', homeLocation: 'dock-recv' }]
246
+ }
247
+ /* 커널을 실제로 굴려 봉투를 얻는다 — 손으로 지어낸 모양은 검증이 아니다. */
248
+ const journal = new EventJournal()
249
+ const k = new WmsKernel('journal-probe')
250
+ k.onEvent((e: any) => journal.append(e))
251
+ k.loadTwinModel(model)
252
+ k.scenario.load({
253
+ seed: 7,
254
+ generators: [
255
+ {
256
+ kind: 'inbound-arrival',
257
+ rate: { distribution: 'constant', meanPerHour: 240 },
258
+ content: { skuMix: [{ gtin: 'urn:epc:idpat:sgtin:0614141.012345.*', weight: 1 }], qtyPerLine: { min: 1, max: 1 } }
259
+ }
260
+ ]
261
+ })
262
+ k.scenario.start()
263
+ for (let i = 0; i < 200; i++) k.tick(1000)
264
+
265
+ const INST = 'journal-probe'
266
+ await TwinEngine.provision(domainId, INST, 'wms', model)
267
+ await TwinEngine.persistBatch(domainId, INST, journal.all(), 0)
268
+ await TwinEngine.stop(domainId, INST).catch(() => {})
269
+ delete (TwinEngine as any).recovered[`${domainId}:${INST}`]
270
+
271
+ const it = await q.twinModelItem(INST, 'equipment', 'fk-1', ctx)
272
+ assert.ok(it?.observed, '저널이 있으면 멈춘 트윈도 마지막으로 알던 사실을 답할 수 있다')
273
+ assert.equal(it.observedBasis, 'journal', '라이브가 아니라 되짚은 것이라고 말해야 "지금 이렇다" 로 읽히지 않는다')
274
+ assert.ok(it.observedRevision > 0, '몇 번째 리비전까지 접었는지 함께 말한다 — 규모의 한계도 그 수가 말한다')
275
+ })
@@ -0,0 +1,176 @@
1
+ /*
2
+ * ★ 다단 전개(BOM) — **자재가 자재를 품는 트리가 아니다.**
3
+ *
4
+ * 표준(ISA-95)이 택한 모양은 자재와 레시피가 번갈아 나오는 그래프다:
5
+ * 완제품 ←산출— 레시피 —투입→ 부품, 그리고 그 부품이 또 어느 레시피의 산출이면 거기서 다시 내려간다.
6
+ * 우리 선언(`twin.rel.input`·`twin.rel.output`)이 이미 그 두 홉이므로 **새 선언 없이** 재귀가 된다.
7
+ *
8
+ * 여기서 지키는 것:
9
+ * · 패턴을 코드가 알지 않는다 — 선언 그래프에서 자기 축으로 돌아오는 순환을 찾는다
10
+ * · **조용히 고르지 않는다** — 순환이 여럿이면 후보를 내고 멈춘다(방향이 뒤집힌 전개는
11
+ * 틀린 답이 아니라 **반대의 답**이라 화면만 보고는 구별되지 않는다)
12
+ * · 수량은 곱해 내려가되, **모르면 곱하지 않는다**(1 로 메우면 윗단 합계가 통째로 거짓이 된다)
13
+ * · 사이클을 만나면 다시 파지 않고 말한다
14
+ * · 상한에서 끊기면 끊겼다고 말한다
15
+ */
16
+ import { test, before, after } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { createRequire } from 'node:module'
19
+ import type { DataSource } from 'typeorm'
20
+
21
+ const req = createRequire(import.meta.url)
22
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
23
+ const { entities: authEntities } = req('@things-factory/auth-base')
24
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
25
+ const { TwinEngine } = req('../dist-server/engine/index.js')
26
+ const { TwinModelTreeQuery } = req('../dist-server/service/twin-model/twin-model-tree-query.js')
27
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
28
+
29
+ let ds: DataSource
30
+ let domainId: string
31
+ let q: any
32
+ let ctx: any
33
+
34
+ const INSTANCE = 'bom-probe'
35
+
36
+ /**
37
+ * 3단 BOM — 완제품 → 하위조립 → 원자재.
38
+ * trailer ← trailer-line ← { frame ×1, axle ×2 }
39
+ * frame ← frame-line ← { steel ×4 }
40
+ */
41
+ const master = (extra: any = {}): any => ({
42
+ source: INSTANCE,
43
+ system: 'mes',
44
+ siteName: 'BOM 공장',
45
+ space: { width: 60, depth: 30, unit: 'm' },
46
+ locations: [{ id: 'line', type: 'weld-station', role: 'location', capacity: 10 }],
47
+ equipment: [{ id: 'welder-1', kind: 'welder', homeLocationId: 'line' }],
48
+ productionSpec: {
49
+ definition: {
50
+ materials: [{ id: 'trailer' }, { id: 'frame' }, { id: 'axle' }, { id: 'steel' }],
51
+ recipes: [
52
+ { key: 'trailer-line', label: '트레일러', inputs: [{ material: 'frame', qty: 1 }, { material: 'axle', qty: 2 }], outputs: [{ material: 'trailer', qty: 1 }] },
53
+ { key: 'frame-line', label: '프레임', inputs: [{ material: 'steel', qty: 4 }], outputs: [{ material: 'frame', qty: 1 }] }
54
+ ],
55
+ ...extra
56
+ }
57
+ }
58
+ })
59
+
60
+ const explode = (itemId: string, opts: any = {}) =>
61
+ q.twinModelTree(INSTANCE, 'materials', itemId, ctx, 'twin.rel.output', 'twin.rel.input', opts.maxDepth, opts.maxItems)
62
+
63
+ before(async () => {
64
+ ds = new TypeOrmDataSource({
65
+ type: 'sqlite',
66
+ database: ':memory:',
67
+ synchronize: true,
68
+ dropSchema: true,
69
+ namingStrategy: new NamingStrategy(),
70
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
71
+ logging: false
72
+ })
73
+ await ds.initialize()
74
+ addDataSource('default', ds)
75
+ const d = await ds.getRepository(Domain).save({ name: 'bom-test', subdomain: 'bom-test' } as any)
76
+ domainId = d.id
77
+ q = new TwinModelTreeQuery()
78
+ ctx = { state: { domain: { id: domainId } } }
79
+ await TwinEngine.ingestMaster(domainId, master())
80
+ })
81
+
82
+ after(async () => {
83
+ await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
84
+ await ds?.destroy()
85
+ })
86
+
87
+ test('다단으로 내려간다 — 자재가 자재를 품지 않아도 전개된다', async () => {
88
+ const t = await explode('trailer')
89
+ const byDepth = (n: number) => t.items.filter((x: any) => x.depth === n).map((x: any) => x.id).sort()
90
+
91
+ assert.deepEqual(byDepth(0), ['trailer'])
92
+ assert.deepEqual(byDepth(1), ['axle', 'frame'], '레시피를 거쳐 1단이 나온다')
93
+ assert.deepEqual(byDepth(2), ['steel'], '프레임이 또 어느 레시피의 산출이므로 거기서 더 내려간다')
94
+ })
95
+
96
+ test('수량을 곱해 내려간다 — 그리고 모르면 곱하지 않는다', async () => {
97
+ const t = await explode('trailer')
98
+ const qty = (id: string) => t.items.find((n: any) => n.id === id)?.qty
99
+ assert.equal(qty('frame'), 1)
100
+ assert.equal(qty('axle'), 2)
101
+ assert.equal(qty('steel'), 4, '프레임 1개당 강재 4 → 트레일러 1대당 4')
102
+ assert.equal(t.qtyIncomplete, false)
103
+
104
+ /* 수량이 빠진 선언이 섞이면 그 아래 합계는 성립하지 않는다 — 1 로 메우지 않고 그렇게 말한다. */
105
+ const noQty = master({
106
+ recipes: [
107
+ { key: 'trailer-line', label: 't', inputs: [{ material: 'frame' }], outputs: [{ material: 'trailer', qty: 1 }] },
108
+ { key: 'frame-line', label: 'f', inputs: [{ material: 'steel', qty: 4 }], outputs: [{ material: 'frame', qty: 1 }] }
109
+ ]
110
+ })
111
+ await TwinEngine.ingestMaster(domainId, noQty)
112
+ const t2 = await explode('trailer')
113
+ assert.equal(t2.items.find((n: any) => n.id === 'frame').qty, null, '모르는 수량을 1 로 메우지 않는다')
114
+ assert.equal(t2.items.find((n: any) => n.id === 'steel').qty, null, '모르는 값 아래는 전부 모르는 값이다')
115
+ assert.equal(t2.qtyIncomplete, true, '합계가 성립하지 않는다는 사실을 낸다')
116
+
117
+ await TwinEngine.ingestMaster(domainId, master())
118
+ })
119
+
120
+ test('역전개도 같은 선언으로 — 방향만 뒤집는다', async () => {
121
+ /* "이 강재를 쓰는 것은 무엇인가" — 투입으로 들어와 산출로 나간다. */
122
+ const up = await q.twinModelTree(INSTANCE, 'materials', 'steel', ctx, 'twin.rel.input', 'twin.rel.output')
123
+ assert.deepEqual(up.items.filter((n: any) => n.depth === 1).map((n: any) => n.id), ['frame'])
124
+ assert.deepEqual(up.items.filter((n: any) => n.depth === 2).map((n: any) => n.id), ['trailer'])
125
+ })
126
+
127
+ test('방향을 안 고르면 아무것도 펼치지 않는다 — 뒤집힌 전개는 틀린 답이 아니라 반대의 답이다', async () => {
128
+ const t = await q.twinModelTree(INSTANCE, 'materials', 'trailer', ctx)
129
+ assert.equal(t.reason, 'ambiguous-pattern')
130
+ assert.equal(t.items.length, 0, '한쪽을 집으면 아무도 모른 채 반대 답을 본다')
131
+ assert.ok(t.patterns.length >= 2, '고를 수 있게 후보를 낸다')
132
+ })
133
+
134
+ test('어떤 순환을 따라 펼쳤는지 밝힌다 — 화면만 보고는 방향을 구별할 수 없다', async () => {
135
+ const t = await explode('trailer')
136
+ assert.equal(t.pattern.inVia, 'twin.rel.output')
137
+ assert.equal(t.pattern.outVia, 'twin.rel.input')
138
+ assert.equal(t.pattern.through, 'recipes')
139
+ })
140
+
141
+ test('사이클은 다시 탐색하지 않고 알린다', async () => {
142
+ /* 강재가 트레일러에서 나온다고 선언하면(재활용) 그래프가 순환한다. 파고 들면 돌아 나오지 못한다. */
143
+ const loop = master({
144
+ recipes: [
145
+ { key: 'trailer-line', label: 't', inputs: [{ material: 'frame', qty: 1 }], outputs: [{ material: 'trailer', qty: 1 }] },
146
+ { key: 'frame-line', label: 'f', inputs: [{ material: 'trailer', qty: 1 }], outputs: [{ material: 'frame', qty: 1 }] }
147
+ ]
148
+ })
149
+ await TwinEngine.ingestMaster(domainId, loop)
150
+ const t = await explode('trailer')
151
+ assert.ok(t.items.length < 10, '무한히 자라지 않는다')
152
+ assert.ok(t.cycles.length > 0, '사이클을 만났다는 사실을 말한다')
153
+ await TwinEngine.ingestMaster(domainId, master())
154
+ })
155
+
156
+ test('상한에서 끊기면 끊겼다고 알린다 — 조용히 자르지 않는다', async () => {
157
+ const shallow = await explode('trailer', { maxDepth: 1 })
158
+ assert.equal(shallow.truncated.depth, true)
159
+ assert.deepEqual(shallow.items.filter((n: any) => n.depth === 2), [], '깊이 상한 아래는 안 판다')
160
+ /* 너비 우선이라 **얕은 층은 온전하다** — 깊이 우선이면 한 가지만 깊게 파고 끊긴다. */
161
+ assert.deepEqual(shallow.items.filter((n: any) => n.depth === 1).map((n: any) => n.id).sort(), ['axle', 'frame'])
162
+ })
163
+
164
+ test('선언에 없는 자재를 참조하면 그 사실이 드러난다', async () => {
165
+ const ghost = master({
166
+ recipes: [
167
+ { key: 'trailer-line', label: 't', inputs: [{ material: 'no-such-part', qty: 1 }], outputs: [{ material: 'trailer', qty: 1 }] }
168
+ ]
169
+ })
170
+ await TwinEngine.ingestMaster(domainId, ghost)
171
+ const t = await explode('trailer')
172
+ const child = t.items.find((n: any) => n.id === 'no-such-part')
173
+ assert.ok(child, '가리켜진 것은 그리되')
174
+ assert.equal(child.known, false, '선언에 없다는 사실을 함께 낸다(있는 척하지 않는다)')
175
+ await TwinEngine.ingestMaster(domainId, master())
176
+ })
@@ -0,0 +1,114 @@
1
+ /*
2
+ * ★ 트윈은 자기 원본을 기억한다 — **기억하지 않으면 다시 읽을 수 없다.**
3
+ *
4
+ * 트윈은 원본을 비출 뿐이므로(ADR-0028) 원본이 달라지면 다시 읽어야 한다. 그런데 트윈은
5
+ * "내가 무엇에서 나왔나" 를 남기지 않았다 — 어댑터로 들어온 트윈에는 `TwinReference` 행이
6
+ * 있었지만 템플릿으로 세운 트윈에는 아무 표식도 없었다. 그 트윈은 **첫날의 모습에 영원히
7
+ * 머문다.** 실제로 이 앱의 트윈 둘이 그 상태였다(`twinReferences` 도 비었고 `source` 도 null).
8
+ *
9
+ * 여기서 지키는 것 셋:
10
+ * · 인제스트가 원천을 각인한다 — 원천이 무엇이든 한 자리에
11
+ * · 원천을 모르는 재프로비전이 **이미 아는 것을 지우지 않는다** — 한 번 지우면 다시 못 읽는다
12
+ * · 못 읽는 트윈은 **못 읽는다고 말한다** — 짐작으로 아무 원본이나 집지 않는다
13
+ */
14
+ import { test, before, after } from 'node:test'
15
+ import assert from 'node:assert/strict'
16
+ import { createRequire } from 'node:module'
17
+ import type { DataSource } from 'typeorm'
18
+
19
+ const req = createRequire(import.meta.url)
20
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
21
+ const { entities: authEntities } = req('@things-factory/auth-base')
22
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
23
+ const { TwinEngine } = req('../dist-server/engine/index.js')
24
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
25
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
26
+
27
+ let ds: DataSource
28
+ let domainId: string
29
+
30
+ const BORN = 'origin-probe'
31
+ const ORPHAN = 'orphan-probe'
32
+
33
+ const master = (source: string, bins: number, origin?: any): any => ({
34
+ source,
35
+ system: 'wms',
36
+ siteName: '원천 창고',
37
+ space: { width: 40, depth: 20, unit: 'm' },
38
+ locations: [
39
+ { id: 'dock-recv', type: 'dock', role: 'location', name: '입고 도크', capacity: 100 },
40
+ ...Array.from({ length: bins }, (_, i) => ({ id: `bin-${i + 1}`, type: 'storage', role: 'location', name: `빈${i + 1}`, capacity: 20 }))
41
+ ],
42
+ equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock-recv' }],
43
+ ...(origin ? { origin } : {})
44
+ })
45
+
46
+ const originOf = async (instanceId: string) =>
47
+ (await ds.getRepository(TwinInstance).findOne({ where: { instanceId } }))?.origin
48
+
49
+ before(async () => {
50
+ ds = new TypeOrmDataSource({
51
+ type: 'sqlite',
52
+ database: ':memory:',
53
+ synchronize: true,
54
+ dropSchema: true,
55
+ namingStrategy: new NamingStrategy(),
56
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
57
+ logging: false
58
+ })
59
+ await ds.initialize()
60
+ addDataSource('default', ds)
61
+ const d = await ds.getRepository(Domain).save({ name: 'origin-test', subdomain: 'origin-test' } as any)
62
+ domainId = d.id
63
+ })
64
+
65
+ after(async () => {
66
+ for (const id of [BORN, ORPHAN]) await TwinEngine.stop(domainId, id).catch(() => {})
67
+ await ds?.destroy()
68
+ })
69
+
70
+ test('인제스트가 원천을 각인한다 — 원천이 무엇이든 한 자리에', async () => {
71
+ await TwinEngine.ingestMaster(domainId, master(BORN, 3, { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } }))
72
+ assert.deepEqual(await originOf(BORN), { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } })
73
+ })
74
+
75
+ test('원천을 모르는 재프로비전이 이미 아는 것을 지우지 않는다 — 한 번 지우면 다시 못 읽는다', async () => {
76
+ /*
77
+ * 이 경로는 실제로 있다 — 원천을 모르는 호출자(스크립트·복구·옛 코드)가 같은 트윈을 다시
78
+ * 인제스트한다. 그때 `origin` 을 덮어 비우면 그 트윈은 **조용히 다시 읽을 수 없게 된다.**
79
+ * 그런 종류의 손상은 다음번에 다시 읽으려 할 때까지 아무도 모른다.
80
+ */
81
+ await TwinEngine.ingestMaster(domainId, master(BORN, 5))
82
+ assert.deepEqual(
83
+ await originOf(BORN),
84
+ { kind: 'template', templateId: 'wh-unit-load', params: { bins: 3 } },
85
+ '원천을 말하지 않은 인제스트는 기존 원천을 보존한다'
86
+ )
87
+
88
+ const inst = await ds.getRepository(TwinInstance).findOne({ where: { instanceId: BORN } })
89
+ assert.equal(inst.model.locations.length, 6, '구조 자체는 새 마스터로 갱신된다(도크 1 + 빈 5)')
90
+ })
91
+
92
+ test('새 원천을 말하면 갱신된다 — 보존은 침묵에만 적용된다', async () => {
93
+ await TwinEngine.ingestMaster(domainId, master(BORN, 5, { kind: 'reference', source: 'oracle-wms' }))
94
+ assert.deepEqual(await originOf(BORN), { kind: 'reference', source: 'oracle-wms' })
95
+ })
96
+
97
+ test('원천 없이 태어난 트윈은 원천이 없다 — 짐작해 채우지 않는다', async () => {
98
+ await TwinEngine.ingestMaster(domainId, master(ORPHAN, 2))
99
+ assert.equal(await originOf(ORPHAN), null, '모르는 것을 아는 척하면 엉뚱한 원본으로 현장을 덮는다')
100
+ })
101
+
102
+ test('조회가 다시 읽을 수 있는지를 먼저 답한다 — 못 하는 것을 못 한다고 알린다', async () => {
103
+ const { TwinModelQuery } = req('../dist-server/service/twin-model/twin-model-query.js')
104
+ const q = new TwinModelQuery()
105
+ const ctx = { state: { domain: { id: domainId } } } as any
106
+
107
+ const born = await q.twinModelSummary(BORN, ctx)
108
+ assert.equal(born.canResync, true)
109
+ assert.deepEqual(born.origin, { kind: 'reference', source: 'oracle-wms' })
110
+
111
+ const orphan = await q.twinModelSummary(ORPHAN, ctx)
112
+ assert.equal(orphan.canResync, false, '버튼을 그대로 내면 누를 때마다 실패한다')
113
+ assert.equal(orphan.origin, null)
114
+ })