@things-factory/headless-twin 10.0.9 → 10.0.11

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 (354) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  2. package/dist-server/engine/canonical-ingest.js +24 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/energy-topology.d.ts +86 -0
  7. package/dist-server/engine/energy-topology.js +144 -0
  8. package/dist-server/engine/energy-topology.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +8 -3
  10. package/dist-server/engine/index.js +10 -3
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/kpi-baseline.d.ts +1 -1
  13. package/dist-server/engine/kpi-baseline.js.map +1 -1
  14. package/dist-server/engine/kpi-fold.d.ts +137 -1
  15. package/dist-server/engine/kpi-fold.js +225 -0
  16. package/dist-server/engine/kpi-fold.js.map +1 -1
  17. package/dist-server/engine/kpi-query.d.ts +1 -1
  18. package/dist-server/engine/kpi-query.js +212 -18
  19. package/dist-server/engine/kpi-query.js.map +1 -1
  20. package/dist-server/engine/kpi-target.d.ts +17 -0
  21. package/dist-server/engine/kpi-target.js +24 -2
  22. package/dist-server/engine/kpi-target.js.map +1 -1
  23. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  24. package/dist-server/engine/live-feed-registry.js +51 -0
  25. package/dist-server/engine/live-feed-registry.js.map +1 -0
  26. package/dist-server/engine/load-meter.d.ts +181 -0
  27. package/dist-server/engine/load-meter.js +267 -0
  28. package/dist-server/engine/load-meter.js.map +1 -0
  29. package/dist-server/engine/local-declarations.d.ts +262 -0
  30. package/dist-server/engine/local-declarations.js +528 -0
  31. package/dist-server/engine/local-declarations.js.map +1 -0
  32. package/dist-server/engine/model-basis.d.ts +39 -6
  33. package/dist-server/engine/model-basis.js +69 -9
  34. package/dist-server/engine/model-basis.js.map +1 -1
  35. package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
  36. package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
  37. package/dist-server/engine/model-vocabulary.js.map +1 -0
  38. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  39. package/dist-server/engine/oee-accumulator.js +26 -1
  40. package/dist-server/engine/oee-accumulator.js.map +1 -1
  41. package/dist-server/engine/operation-basis.d.ts +32 -0
  42. package/dist-server/engine/operation-basis.js +87 -0
  43. package/dist-server/engine/operation-basis.js.map +1 -0
  44. package/dist-server/engine/property-effects.d.ts +30 -0
  45. package/dist-server/engine/property-effects.js +192 -0
  46. package/dist-server/engine/property-effects.js.map +1 -0
  47. package/dist-server/engine/runtime-key.d.ts +1 -1
  48. package/dist-server/engine/runtime-key.js +1 -1
  49. package/dist-server/engine/runtime-key.js.map +1 -1
  50. package/dist-server/engine/spec-coverage.js +3 -3
  51. package/dist-server/engine/spec-coverage.js.map +1 -1
  52. package/dist-server/engine/state-axes.d.ts +7 -2
  53. package/dist-server/engine/state-axes.js +8 -3
  54. package/dist-server/engine/state-axes.js.map +1 -1
  55. package/dist-server/engine/structure-diff.js +1 -1
  56. package/dist-server/engine/structure-diff.js.map +1 -1
  57. package/dist-server/engine/travel-estimator.d.ts +9 -2
  58. package/dist-server/engine/travel-estimator.js +11 -6
  59. package/dist-server/engine/travel-estimator.js.map +1 -1
  60. package/dist-server/engine/twin-engine.d.ts +332 -47
  61. package/dist-server/engine/twin-engine.js +1314 -279
  62. package/dist-server/engine/twin-engine.js.map +1 -1
  63. package/dist-server/engine/warm-start.d.ts +38 -4
  64. package/dist-server/engine/warm-start.js +38 -9
  65. package/dist-server/engine/warm-start.js.map +1 -1
  66. package/dist-server/index.d.ts +1 -0
  67. package/dist-server/index.js +17 -11
  68. package/dist-server/index.js.map +1 -1
  69. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  70. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  71. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  72. package/dist-server/migrations/index.d.ts +2 -0
  73. package/dist-server/migrations/index.js +10 -0
  74. package/dist-server/migrations/index.js.map +1 -0
  75. package/dist-server/service/index.d.ts +3 -2
  76. package/dist-server/service/index.js +25 -17
  77. package/dist-server/service/index.js.map +1 -1
  78. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  79. package/dist-server/service/reference/discovery-result.js +2 -2
  80. package/dist-server/service/reference/discovery-result.js.map +1 -1
  81. package/dist-server/service/reference/ingest-space.d.ts +24 -1
  82. package/dist-server/service/reference/ingest-space.js +17 -1
  83. package/dist-server/service/reference/ingest-space.js.map +1 -1
  84. package/dist-server/service/reference/reference-live.d.ts +23 -0
  85. package/dist-server/service/reference/reference-live.js +121 -4
  86. package/dist-server/service/reference/reference-live.js.map +1 -1
  87. package/dist-server/service/reference/reference-master.d.ts +194 -13
  88. package/dist-server/service/reference/reference-master.js +159 -28
  89. package/dist-server/service/reference/reference-master.js.map +1 -1
  90. package/dist-server/service/reference/reference-resolver.d.ts +31 -1
  91. package/dist-server/service/reference/reference-resolver.js +223 -12
  92. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  93. package/dist-server/service/reference/template-registry.d.ts +19 -3
  94. package/dist-server/service/reference/template-registry.js.map +1 -1
  95. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  96. package/dist-server/service/twin-audit/command-audit.js +53 -0
  97. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  98. package/dist-server/service/twin-audit/index.d.ts +4 -0
  99. package/dist-server/service/twin-audit/index.js +8 -0
  100. package/dist-server/service/twin-audit/index.js.map +1 -0
  101. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  102. package/dist-server/service/twin-audit/twin-audit-event.js +141 -0
  103. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  104. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  105. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  106. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  107. package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
  108. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  109. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  110. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  111. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  112. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  113. package/dist-server/service/twin-event/twin-event.js +45 -13
  114. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  115. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  116. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  117. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  118. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  119. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  120. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  121. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  122. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  123. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  124. package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
  125. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  126. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  127. package/dist-server/service/twin-instance/twin-instance.js +66 -20
  128. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  129. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
  130. package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
  131. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
  133. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
  134. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  135. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  136. package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
  137. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  138. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  139. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  140. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  141. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  142. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  143. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  144. package/dist-server/service/twin-model/index.d.ts +13 -0
  145. package/dist-server/service/twin-model/index.js +29 -0
  146. package/dist-server/service/twin-model/index.js.map +1 -0
  147. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  148. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  149. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  150. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  151. package/dist-server/service/twin-model/project-structure.js +173 -0
  152. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  153. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  154. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  155. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  157. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  158. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  160. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  161. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  162. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  163. package/dist-server/service/twin-model/twin-location.js +130 -0
  164. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  165. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  166. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  167. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  168. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  169. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  170. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  172. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  173. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  176. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  178. package/dist-server/service/twin-model/twin-operation.js +137 -0
  179. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  180. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  181. package/dist-server/service/twin-space/move-space.js +166 -0
  182. package/dist-server/service/twin-space/move-space.js.map +1 -0
  183. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  184. package/dist-server/service/twin-space/space-integrity.js +182 -0
  185. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  186. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  187. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  188. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  189. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  190. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  191. package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
  192. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  193. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  194. package/dist-server/service/twin-space/twin-space.js +15 -5
  195. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  196. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  197. package/dist-server/service/twin-structure/twin-structure.js +8 -8
  198. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  199. package/dist-shared/axis-read.d.ts +12 -0
  200. package/dist-shared/axis-read.js +32 -0
  201. package/dist-shared/axis-read.js.map +1 -0
  202. package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
  203. package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
  204. package/dist-shared/entity-delta.js.map +1 -0
  205. package/dist-shared/kpi-broadcast.js.map +1 -0
  206. package/dist-shared/twin-level.js.map +1 -0
  207. package/package.json +14 -12
  208. package/server/engine/canonical-ingest.ts +36 -4
  209. package/server/engine/command-routing.ts +1 -1
  210. package/server/engine/energy-topology.ts +189 -0
  211. package/server/engine/index.ts +10 -3
  212. package/server/engine/kpi-baseline.ts +1 -1
  213. package/server/engine/kpi-fold.ts +389 -1
  214. package/server/engine/kpi-query.ts +214 -19
  215. package/server/engine/kpi-target.ts +24 -2
  216. package/server/engine/live-feed-registry.ts +58 -0
  217. package/server/engine/load-meter.ts +384 -0
  218. package/server/engine/local-declarations.ts +700 -0
  219. package/server/engine/model-basis.ts +78 -10
  220. package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
  221. package/server/engine/oee-accumulator.ts +34 -1
  222. package/server/engine/operation-basis.ts +100 -0
  223. package/server/engine/property-effects.ts +199 -0
  224. package/server/engine/runtime-key.ts +1 -1
  225. package/server/engine/spec-coverage.ts +3 -3
  226. package/server/engine/state-axes.ts +8 -3
  227. package/server/engine/structure-diff.ts +1 -1
  228. package/server/engine/travel-estimator.ts +11 -6
  229. package/server/engine/twin-engine.ts +1414 -274
  230. package/server/engine/warm-start.ts +83 -12
  231. package/server/index.ts +13 -10
  232. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  233. package/server/migrations/index.ts +7 -0
  234. package/server/service/index.ts +9 -1
  235. package/server/service/reference/discovery-result.ts +2 -2
  236. package/server/service/reference/ingest-space.ts +35 -1
  237. package/server/service/reference/reference-live.ts +119 -4
  238. package/server/service/reference/reference-master.ts +276 -35
  239. package/server/service/reference/reference-resolver.ts +232 -12
  240. package/server/service/reference/template-registry.ts +20 -5
  241. package/server/service/twin-audit/command-audit.ts +81 -0
  242. package/server/service/twin-audit/index.ts +5 -0
  243. package/server/service/twin-audit/twin-audit-event.ts +138 -0
  244. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  245. package/server/service/twin-control/twin-control-mutation.ts +22 -1
  246. package/server/service/twin-event/twin-event-keys.ts +1 -1
  247. package/server/service/twin-event/twin-event.ts +59 -14
  248. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  249. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  250. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  251. package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
  252. package/server/service/twin-instance/twin-instance.ts +126 -25
  253. package/server/service/twin-journal/twin-journal-query.ts +74 -10
  254. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
  255. package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
  256. package/server/service/twin-model/epcis-coverage.ts +195 -0
  257. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  258. package/server/service/twin-model/index.ts +25 -0
  259. package/server/service/twin-model/isa95-coverage.ts +190 -0
  260. package/server/service/twin-model/project-structure.ts +199 -0
  261. package/server/service/twin-model/standard-coverage.ts +35 -0
  262. package/server/service/twin-model/twin-equipment.ts +157 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +147 -0
  265. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  266. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  267. package/server/service/twin-model/twin-model-query.ts +537 -0
  268. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  269. package/server/service/twin-model/twin-operation.ts +157 -0
  270. package/server/service/twin-space/move-space.ts +262 -0
  271. package/server/service/twin-space/space-integrity.ts +293 -0
  272. package/server/service/twin-space/twin-space-area.ts +4 -4
  273. package/server/service/twin-space/twin-space-representation.ts +6 -6
  274. package/server/service/twin-space/twin-space-resolver.ts +355 -21
  275. package/server/service/twin-space/twin-space.ts +40 -6
  276. package/server/service/twin-structure/twin-structure.ts +10 -10
  277. package/shared/axis-read.ts +28 -0
  278. package/{server/engine → shared}/entity-delta.ts +96 -1
  279. package/test/adopt-structure-live.test.ts +133 -0
  280. package/test/attention-digest.test.ts +3 -3
  281. package/test/axis-read.test.ts +87 -0
  282. package/test/boot-resume.test.ts +205 -0
  283. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  284. package/test/capability-mapping.test.ts +1 -1
  285. package/test/column-type-portability.test.ts +122 -0
  286. package/test/command-routing.test.ts +1 -1
  287. package/test/declaration-reaches-model.test.ts +178 -0
  288. package/test/discovery-result.test.ts +1 -1
  289. package/test/energy-topology.test.ts +113 -0
  290. package/test/entity-delta.test.ts +171 -1
  291. package/test/event-time-column.test.ts +67 -0
  292. package/test/forecast-metrics.test.ts +71 -0
  293. package/test/forecast-tuning.test.ts +42 -0
  294. package/test/gap-analytics.test.ts +34 -7
  295. package/test/ingest-bench.test.ts +6 -6
  296. package/test/ingest-running-guard.test.ts +136 -0
  297. package/test/ingest-space.test.ts +29 -1
  298. package/test/instance-cache-lifecycle.test.ts +110 -0
  299. package/test/kernel-kind-guard.test.ts +96 -0
  300. package/test/kpi-baseline-db.test.ts +3 -1
  301. package/test/kpi-baseline.test.ts +3 -3
  302. package/test/kpi-fold.test.ts +393 -2
  303. package/test/kpi-query-bench.test.ts +5 -3
  304. package/test/lineage-survives-restart.test.ts +203 -0
  305. package/test/live-feed-registry.test.ts +67 -0
  306. package/test/live-kernel-facts.test.ts +161 -0
  307. package/test/live-mirror-parity.test.ts +22 -15
  308. package/test/load-meter.test.ts +314 -0
  309. package/test/local-declarations.test.ts +554 -0
  310. package/test/master-to-twin.test.ts +121 -33
  311. package/test/model-basis.test.ts +56 -1
  312. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  313. package/test/move-space.test.ts +149 -0
  314. package/test/mutation-gate.test.ts +19 -5
  315. package/test/oee-accumulator.test.ts +26 -7
  316. package/test/operation-basis.test.ts +100 -0
  317. package/test/operations-capability-db.test.ts +165 -0
  318. package/test/project-structure-db.test.ts +226 -0
  319. package/test/projection-reaches-screen.test.ts +272 -0
  320. package/test/property-effects.test.ts +96 -0
  321. package/test/resync-origin-site.test.ts +77 -0
  322. package/test/scale-twin-bench.test.ts +5 -5
  323. package/test/snapshot-freshness.test.ts +60 -0
  324. package/test/space-integrity.test.ts +223 -0
  325. package/test/standard-coverage.test.ts +94 -0
  326. package/test/state-axes.test.ts +1 -1
  327. package/test/streamline-e2e.test.ts +14 -14
  328. package/test/structure-revision-db.test.ts +11 -11
  329. package/test/tenant-registry-db.test.ts +5 -5
  330. package/test/twin-audit.test.ts +83 -0
  331. package/test/twin-model-item-db.test.ts +275 -0
  332. package/test/twin-model-tree-db.test.ts +176 -0
  333. package/test/twin-origin-resync.test.ts +114 -0
  334. package/test/warm-start-seam.test.ts +6 -6
  335. package/test/warm-start.test.ts +88 -3
  336. package/tsconfig.json +6 -1
  337. package/tsconfig.shared.json +23 -0
  338. package/tsconfig.shared.tsbuildinfo +1 -0
  339. package/tsconfig.tsbuildinfo +1 -0
  340. package/dist-server/engine/board-vocabulary.js.map +0 -1
  341. package/dist-server/engine/entity-delta.js.map +0 -1
  342. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  343. package/dist-server/engine/twin-level.js.map +0 -1
  344. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  345. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  346. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  347. package/dist-server/tsconfig.tsbuildinfo +0 -1
  348. package/server/service/twin-event/backfill-keys.ts +0 -72
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  350. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  352. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  353. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  354. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -8,7 +8,7 @@ import assert from 'node:assert/strict'
8
8
 
9
9
  import { WmsKernel, StateProjector } from '@operato/twin-kernel'
10
10
  import { masterToTwin } from '../server/service/reference/reference-master.ts'
11
- import { buildEntityDeltas } from '../server/engine/entity-delta.ts'
11
+ import { buildEntityDeltas } from '../shared/entity-delta.ts'
12
12
  import { OeeAccumulator, withLiveOee } from '../server/engine/oee-accumulator.ts'
13
13
 
14
14
  const BASE_EPOCH = Date.parse('2026-01-01T00:00:00Z') // 커널 now() 고정 오프셋(flow-engine BASE_EPOCH)
@@ -28,15 +28,15 @@ function runSimCollect() {
28
28
  ],
29
29
  equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock-recv', mtbfMs: 5000, mttrMs: 8000 }]
30
30
  }
31
- const board = masterToTwin(m).board
31
+ const model = masterToTwin(m).model
32
32
  const k = new WmsKernel('t')
33
33
  const events: any[] = []
34
34
  k.onEvent((e: any) => events.push(e))
35
- k.loadBoard(board)
35
+ k.loadTwinModel(model)
36
36
  k.scenario.load({ seed: 9, generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 300 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 1, max: 1 } } }] })
37
37
  k.scenario.start()
38
38
  for (let i = 0; i < 200; i++) k.tick(TICK)
39
- return { events, snap: k.getSnapshot() as any, board }
39
+ return { events, snap: k.getSnapshot() as any, model }
40
40
  }
41
41
 
42
42
  test('★ OEE 누적기: downMs 정확 · busyMs=run+setup · 근사 OEE 유효', () => {
@@ -83,8 +83,8 @@ test('★ OEE 누적기: quality.output 델타 소비 → quality 정확 (good/s
83
83
  })
84
84
 
85
85
  test('★ withLiveOee: projector 미러(oee 없음) + 누적기 → live 설비 payload 에 oee 포함 (원 갭 종료)', () => {
86
- const { events, board } = runSimCollect()
87
- const proj = new StateProjector(board)
86
+ const { events, model } = runSimCollect()
87
+ const proj = new StateProjector(model)
88
88
  const acc = new OeeAccumulator()
89
89
  for (const e of events) { proj.apply(e); acc.apply(e) }
90
90
 
@@ -111,7 +111,7 @@ test('★ withLiveOee: projector 미러(oee 없음) + 누적기 → live 설비
111
111
  * 태운다 — 이 테스트는 그 재계산이 라이브와 **같은 값**을 내는지, 그리고 시간여행에서 **그 시점까지만**
112
112
  * 세는지를 못 박는다.
113
113
  */
114
- test('★ 저널을 다시 태우면 라이브와 같은 계측이 나온다 — 규칙이 한 벌이므로', () => {
114
+ test('★ 저널을 다시 태우면 라이브와 같은 계측이 나온다 — 규칙이 하나이므로', () => {
115
115
  const T0 = Date.parse('2026-08-05T00:00:00.000Z')
116
116
  const ev = (ms: number, id: string, status: string) => ({
117
117
  eventId: `e${ms}`,
@@ -164,3 +164,22 @@ test('★ 계측 시점을 벽시계로 재지 않는다 — 과거 화면의
164
164
  const atMuchLater = acc.oee('fk-1', T0 + 10_000_000)
165
165
  assert.notDeepEqual(atThen, atMuchLater, '시점을 넘기면 값이 달라진다 — 그래서 그 화면의 시각을 넘겨야 한다')
166
166
  })
167
+
168
+ /*
169
+ * ★ 못 본 설비의 계측 창 — **없으면 없다고 한다.**
170
+ *
171
+ * 예전에는 `metricsSinceMs: 0` 을 돌려줬다. 시각이 epoch 밀리초라서 그 창은 곧 "1970년부터" 가 되고,
172
+ * 공식의 분모가 지금 시각 전체가 된다 — 화면에 `idleMs 1767226603000`(약 56,000년) 유휴가 섰다.
173
+ * 그 값은 availability·overall 의 분모이기도 해서 **성과 숫자 전체가 조용히 거짓**이 됐다.
174
+ */
175
+ test('이벤트를 못 본 설비는 계측 창을 만들지 않는다 — epoch 를 창으로 삼지 않는다', () => {
176
+ const acc = new OeeAccumulator()
177
+ const c = acc.counters('never-seen', Date.parse('2026-01-01T00:00:00.000Z'))
178
+ assert.equal(c.metricsSinceMs, undefined, '모르는 창을 0 으로 메우면 1970년부터 쟀다는 말이 된다')
179
+
180
+ /*
181
+ * 그다음(창이 없을 때 유휴는 0)은 **공식의 책임**이고 공식은 커널에 있다 — 커널의
182
+ * `test/oee-window` 가 못 박는다. 여기서 그 결과까지 단정하면 이 검사는 커널 발행 시점에
183
+ * 묶이고, 남의 책임을 자기 검사에 넣은 것이 된다.
184
+ */
185
+ })
@@ -0,0 +1,100 @@
1
+ /*
2
+ * 공정 목록 — **무엇을 돌리고, 그 시간이 어디서 오나.**
3
+ *
4
+ * ── 왜 이 목록이 필요한가 (2026-08-18) ─────────────────────────────────────
5
+ * 「측정이 없을 때의 표준 시간」을 현장이 선언하려면 먼저 무엇에 선언할지 보여야 한다. 그런데 실측으로
6
+ * 운영 중인 트윈 35개 중 **창고·야드는 공정 명세 행이 0개**였다 — 모델만 보면 「공정이 없다」로 읽힌다.
7
+ * 실제로는 `putaway`·`spot` 을 돌리고 있고, 그 사실은 **커널만 안다**(`specCoverage`).
8
+ *
9
+ * 그래서 이 모듈이 지키는 것:
10
+ * ① 목록을 지어내지 않는다 — 원천 행·커널 보고·우리 선언, 세 곳에서만 모은다.
11
+ * ② 돌지 않은 트윈에서 「공정이 없다」고 말하지 않는다 — `basis` 가 비는 것으로 모름을 드러낸다.
12
+ * ③ 선언해 둔 공정이 목록에서 사라지지 않는다(그러면 철회할 자리가 없어진다).
13
+ */
14
+ import { test } from 'node:test'
15
+ import assert from 'node:assert/strict'
16
+
17
+ import { readFileSync } from 'node:fs'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ import { operationBasis, knownOperationKinds } from '../server/engine/operation-basis.ts'
21
+
22
+ /** 커널 흉내 — 자기보고만 낸다(이 모듈이 커널에서 읽는 것은 그것뿐이다). */
23
+ const kernelReporting = (ops: { kind: string; duration: 'measured' | 'declared' | 'default' }[]) => ({
24
+ specCoverage: () => ({
25
+ operations: ops.map(o => ({ ...o, parameters: [] })),
26
+ measuredDurations: ops.filter(o => o.duration === 'measured').length,
27
+ declaredDurations: ops.filter(o => o.duration === 'declared').length,
28
+ defaultDurations: ops.filter(o => o.duration === 'default').length
29
+ })
30
+ })
31
+
32
+ test('행이 없는 트윈에서도 돌린 공정이 나온다 — 창고·야드가 바로 그 경우다', () => {
33
+ const rows = operationBasis({ locations: [], equipment: [] }, kernelReporting([{ kind: 'putaway', duration: 'default' }]))
34
+ assert.deepEqual(rows, [{ kind: 'putaway', basis: 'default' }])
35
+ })
36
+
37
+ test('원천 명세 행은 이름과 시간을 함께 든다 — 우리 선언이 그것을 이긴다', () => {
38
+ const model = { operations: [{ key: 'weld', label: 'Welding', intent: 'process', duration: 'PT90S' }], localDurations: { weld: 'PT60S' } }
39
+ const rows = operationBasis(model, kernelReporting([{ kind: 'weld', duration: 'declared' }]))
40
+ assert.deepEqual(rows, [{ kind: 'weld', label: 'Welding', referenceDuration: 'PT90S', basis: 'declared', localDuration: 'PT60S' }])
41
+ })
42
+
43
+ test('정지 트윈은 basis 가 빈다 — 「공정이 없다」가 아니라 「모른다」다', () => {
44
+ const rows = operationBasis({ operations: [{ key: 'pick', label: 'Pick' }] }, undefined)
45
+ assert.deepEqual(rows, [{ kind: 'pick', label: 'Pick' }])
46
+ assert.equal(rows[0].basis, undefined)
47
+ })
48
+
49
+ test('선언해 둔 공정은 커널이 아직 돌리지 않아도 목록에 남는다 — 철회할 자리가 사라지지 않게', () => {
50
+ const rows = operationBasis({ localDurations: { spot: 'PT300S' } }, kernelReporting([]))
51
+ assert.deepEqual(rows, [{ kind: 'spot', localDuration: 'PT300S' }])
52
+ })
53
+
54
+ test('아무 근거도 없으면 목록을 지어내지 않는다 — 빈 배열이다', () => {
55
+ assert.deepEqual(operationBasis({}, undefined), [])
56
+ })
57
+
58
+ test('확인 목록은 하나도 없을 때 undefined 다 — 빈 배열은 「아무것도 안 된다」로 읽힌다', () => {
59
+ assert.equal(knownOperationKinds({}, undefined), undefined)
60
+ assert.deepEqual(knownOperationKinds({}, kernelReporting([{ kind: 'putaway', duration: 'default' }])), ['putaway'])
61
+ })
62
+
63
+ test('커널이 보고하지 못해도 터지지 않는다 — 옛 커널에서도 목록은 나온다', () => {
64
+ const rows = operationBasis({ operations: [{ key: 'pick', label: 'Pick' }] }, { tick: () => {} })
65
+ assert.deepEqual(rows, [{ kind: 'pick', label: 'Pick' }])
66
+ })
67
+
68
+ /*
69
+ * ── 재기동 직후의 빈칸 (2026-08-18 실측) ────────────────────────────────────
70
+ * 커널의 자기보고는 **작업이 새로 생길 때** 채워진다. 그래서 이력이 풍부한 창고 트윈(pick·putaway 를
71
+ * 이미 배운 트윈)도 재기동 직후에는 목록이 비었다 — 값을 채울 곳을 보여야 하는 화면이 그때 아무 말도
72
+ * 못 한다. 이력에서 배운 종류를 넷째 근거로 넣어 그 빈칸을 메운다.
73
+ */
74
+ test('이력에서 배운 종류가 목록에 나온다 — 재기동 직후에도 빈칸이 아니다', () => {
75
+ const rows = operationBasis({}, kernelReporting([]), ['pick', 'putaway'])
76
+ assert.deepEqual(rows, [{ kind: 'pick', basis: 'measured' }, { kind: 'putaway', basis: 'measured' }])
77
+ })
78
+
79
+ test('커널이 실제로 돌린 사실이 이력 목록을 덮지 않는다 — 더 강한 사실이 이긴다', () => {
80
+ const rows = operationBasis({}, kernelReporting([{ kind: 'putaway', duration: 'default' }]), ['putaway'])
81
+ assert.deepEqual(rows, [{ kind: 'putaway', basis: 'default' }], '지금 상수로 돌고 있으면 그렇게 말한다')
82
+ })
83
+
84
+ test('이력 목록은 선언 대조에도 쓰인다 — 오타를 그때부터 잡는다', () => {
85
+ assert.deepEqual(knownOperationKinds({}, undefined, ['pick']), ['pick'])
86
+ })
87
+
88
+ /*
89
+ * ── 도는 시뮬레이션은 「저장했다」가 아니다 (2026-08-18 실측) ────────────────
90
+ * 시뮬 기동 경로가 `mode` 를 비워 두어(「미지정=sim」 규약에 기대) `modeOf()` 가 도는 트윈에 undefined 를
91
+ * 답했다. 그래서 선언·철회의 답이 「저장했다」로 나갔다 — 실제로는 도는 커널이 옛 수를 쓰고 재기동 때
92
+ * 받는데, 사용자는 지금 화면의 수가 새 값으로 계산된 줄로 읽는다. 사실을 적는 쪽으로 고쳤다.
93
+ */
94
+ test('시뮬 기동이 모드를 적는다 — 「돌고 있나」를 묻는 쪽이 답을 받아야 한다', () => {
95
+ const src = readFileSync(fileURLToPath(new URL('../server/engine/twin-engine.ts', import.meta.url)), 'utf-8')
96
+ assert.match(src, /id, domainId, mode: 'sim', runtime/, '시뮬 런타임도 모드를 든다')
97
+ /* 답이 갈리는 자리 — 도는 트윈이면 「재기동 때 받는다」로 말해야 한다. */
98
+ const mut = readFileSync(fileURLToPath(new URL('../server/service/twin-model/twin-model-mutation.ts', import.meta.url)), 'utf-8')
99
+ assert.match(mut, /running \? 'on-restart' : 'stored'/)
100
+ })
@@ -0,0 +1,165 @@
1
+ /*
2
+ * ★ 생산 능력 질의 — **트윈 하나를 두고 "지금 더 받을 수 있나" 를 물을 길.**
3
+ *
4
+ * 커널이 계산과 표준 등식을 지킨다(kernel/test/operations-capability). 이 검사가 지키는 것은 그 앞의
5
+ * 배선이다 — 그리고 여기서 지켜야 할 규율은 `twinCapacity` 와 같다:
6
+ *
7
+ * · **대상은 등록부가 정한다** — 떠 있는 런타임만 훑으면 "안 떠 있어서 안 보이는 것" 과 "없는 것" 이
8
+ * 구별되지 않는다. 화면은 그 둘을 다르게 말해야 한다.
9
+ * · **공정을 선언하지 않은 트윈은 판정 대상이 아니다** — 0 으로 채우면 "야드가 병목" 이라는 없는
10
+ * 사실이 생긴다.
11
+ * · **구간은 부르는 쪽이 말한다** — 기본값을 두면 화면마다 다른 구간을 보면서 같은 값으로 읽는다.
12
+ */
13
+ import { test, before, after } from 'node:test'
14
+ import assert from 'node:assert/strict'
15
+ import { createRequire } from 'node:module'
16
+ import type { DataSource } from 'typeorm'
17
+
18
+ const req = createRequire(import.meta.url)
19
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
20
+ const { entities: authEntities } = req('@things-factory/auth-base')
21
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
22
+ const { TwinEngine } = req('../dist-server/engine/index.js')
23
+ const { TwinJournalQuery } = req('../dist-server/service/twin-journal/twin-journal-query.js')
24
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
25
+
26
+ let ds: DataSource
27
+ let domainId: string
28
+ let q: any
29
+ let ctx: any
30
+
31
+ const MES = 'cap-mes'
32
+ const WMS = 'cap-wms'
33
+ const WINDOW = { fromTime: '2026-01-05T00:00:00.000Z', toTime: '2026-01-06T00:00:00.000Z' }
34
+
35
+ /** 절단 하나 — 설비 2대·사람 2명, 사이클 1시간. 24시간 창이면 상한 48대. */
36
+ const mesMaster = (): any => ({
37
+ source: MES,
38
+ system: 'mes',
39
+ siteName: '능력 공장',
40
+ space: { spaceId: 'cap-site', width: 60, depth: 40, unit: 'm' },
41
+ locations: [
42
+ { id: 'raw-store', type: 'raw-store', role: 'location', name: '자재 보관', capacity: 100 },
43
+ { id: 'cut-station', type: 'cut-station', role: 'location', name: '절단', capacity: 10 },
44
+ { id: 'fg-store', type: 'fg-store', role: 'location', name: '완제품', capacity: 100 }
45
+ ],
46
+ equipment: [
47
+ { id: 'cutter-1', kind: 'cutter', homeLocationId: 'cut-station' },
48
+ { id: 'cutter-2', kind: 'cutter', homeLocationId: 'cut-station' }
49
+ ],
50
+ personnelClasses: [{ id: 'cutter-op', description: '절단 작업자' }],
51
+ persons: [
52
+ { id: 'c-1', personnelClassIds: ['cutter-op'], homeLocationId: 'cut-station' },
53
+ { id: 'c-2', personnelClassIds: ['cutter-op'], homeLocationId: 'cut-station' }
54
+ ],
55
+ operations: [
56
+ {
57
+ key: 'cut', label: '절단', intent: 'process', locationType: 'cut-station', resourceType: 'cutter',
58
+ duration: 'PT1H',
59
+ equipmentSpecification: [{ equipmentClass: 'cutter', quantity: 1 }],
60
+ personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }]
61
+ }
62
+ ],
63
+ productionSpec: {
64
+ /* 레시피 생산은 자재 키를 GS1 로 풀 수 있어야 한다 — 커널이 없으면 기동을 거절한다(정확한 거절이다). */
65
+ companyPrefix: '0614141',
66
+ binding: { steel: '077101', frame: '078101' },
67
+ definition: {
68
+ materials: [{ id: 'steel' }, { id: 'frame' }],
69
+ routes: [{ key: 'r', steps: ['cut'] }],
70
+ recipes: [{ key: 'rc', label: '프레임', route: 'r', inputs: [{ material: 'steel', qty: 1 }], outputs: [{ material: 'frame', qty: 1 }] }]
71
+ }
72
+ }
73
+ })
74
+
75
+ /** 공정을 선언하지 않는 창고 — 판정 대상이 아님을 확인하는 대조군. */
76
+ const wmsMaster = (): any => ({
77
+ source: WMS,
78
+ system: 'wms',
79
+ siteName: '능력 창고',
80
+ space: { spaceId: 'cap-wh', width: 40, depth: 30, unit: 'm' },
81
+ locations: [
82
+ { id: 'dock', type: 'dock', role: 'location', name: '도크', capacity: 50 },
83
+ { id: 'bin-1', type: 'storage', role: 'location', name: '빈1', capacity: 20 }
84
+ ],
85
+ equipment: [{ id: 'fk-1', kind: 'forklift', homeLocationId: 'dock' }]
86
+ })
87
+
88
+ before(async () => {
89
+ ds = new TypeOrmDataSource({
90
+ type: 'sqlite',
91
+ database: ':memory:',
92
+ synchronize: true,
93
+ dropSchema: true,
94
+ namingStrategy: new NamingStrategy(),
95
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
96
+ logging: false
97
+ })
98
+ await ds.initialize()
99
+ addDataSource('default', ds)
100
+ const d = await ds.getRepository(Domain).save({ name: 'cap-test', subdomain: 'cap-test' } as any)
101
+ domainId = d.id
102
+ q = new TwinJournalQuery()
103
+ ctx = { state: { domain: { id: domainId } } }
104
+
105
+ await TwinEngine.ingestMaster(domainId, mesMaster())
106
+ await TwinEngine.ingestMaster(domainId, wmsMaster())
107
+ })
108
+
109
+ after(async () => {
110
+ for (const id of [MES, WMS]) await TwinEngine.stop(domainId, id).catch(() => {})
111
+ await ds?.destroy()
112
+ })
113
+
114
+ const ask = (instanceId: string) =>
115
+ q.twinOperationsCapability(instanceId, undefined, 24, WINDOW.fromTime, WINDOW.toTime, WINDOW.fromTime, ctx)
116
+
117
+ test('실행 중이 아니면 그 사실을 알린다 — "없는 것" 과 구별된다', async () => {
118
+ const rows = await ask(MES)
119
+ assert.equal(rows.length, 1, '등록부에 있으면 목록에 선다(떠 있지 않아도)')
120
+ assert.equal(rows[0].reason, 'not-running')
121
+ assert.equal(rows[0].report, undefined, '값을 지어내지 않는다')
122
+ })
123
+
124
+ test('돌고 있으면 구간의 양을 종류로 갈라 낸다 — 표준 등식이 성립한다', async () => {
125
+ await TwinEngine.startFromRegistry(domainId, MES, 'sim-experiment')
126
+ const rows = await ask(MES)
127
+ assert.equal(rows[0].reason, undefined)
128
+
129
+ const report = rows[0].report
130
+ assert.equal(report.operationsType, 'Production', '표준 열거값을 그대로 낸다')
131
+ assert.equal(report.window.fromTime, WINDOW.fromTime)
132
+ assert.equal(report.window.workingHours, 24, '캘린더 선언이 없으면 종일 가동으로 본다')
133
+
134
+ const cut = report.perOperation.find((o: any) => o.operation === 'cut')
135
+ assert.ok(cut, '선언한 공정이 보고서에 있어야 한다')
136
+ assert.equal(cut.total, 48, '설비 2대·사람 2명·사이클 1시간 → 하루 48대')
137
+ assert.equal(cut.total, cut.committed + cut.available + cut.unattainable, 'Total = Committed + Available + Unattainable')
138
+ })
139
+
140
+ test('공정을 선언하지 않은 트윈은 판정 대상이 아니다 — 0 으로 채우면 없는 병목이 생긴다', async () => {
141
+ await TwinEngine.startFromRegistry(domainId, WMS, 'sim-experiment')
142
+ const rows = await q.twinOperationsCapability(WMS, undefined, 24, WINDOW.fromTime, WINDOW.toTime, WINDOW.fromTime, ctx)
143
+ assert.equal(rows[0].reason, 'no-operations')
144
+ assert.equal(rows[0].report, undefined)
145
+ })
146
+
147
+ test('구간은 호출하는 쪽이 지정한다 — 뒤집힌 구간·잘못된 시각은 거절한다', async () => {
148
+ await assert.rejects(
149
+ () => q.twinOperationsCapability(MES, undefined, 24, WINDOW.toTime, WINDOW.fromTime, undefined, ctx),
150
+ /inverted/
151
+ )
152
+ await assert.rejects(
153
+ () => q.twinOperationsCapability(MES, undefined, 24, 'not-a-time', WINDOW.toTime, undefined, ctx),
154
+ /valid ISO time/
155
+ )
156
+ await assert.rejects(() => q.twinOperationsCapability(MES, undefined, 0, WINDOW.fromTime, WINDOW.toTime, undefined, ctx))
157
+ })
158
+
159
+ test('남의 테넌트 트윈은 열리지 않는다 — 능력은 현장의 사실이다', async () => {
160
+ const other = { state: { domain: { id: 'no-such-domain' } } }
161
+ await assert.rejects(
162
+ () => q.twinOperationsCapability(MES, undefined, 24, WINDOW.fromTime, WINDOW.toTime, undefined, other),
163
+ /not found in this tenant/
164
+ )
165
+ })
@@ -0,0 +1,226 @@
1
+ /*
2
+ * 구조 투영 — `TwinModelDef`(커널 입력 문서) → **표준 엔티티 행**(ADR-0032).
3
+ *
4
+ * ── 왜 DB 검사인가 ─────────────────────────────────────────────────────────
5
+ * 이 코드의 값어치는 **DB 가 대신 지켜 주는 것**에 있다 — 끊어진 참조를 FK 가 막고, 원본 없는 행을
6
+ * NOT NULL 이 막는다. 순수 함수 검사로는 그게 하나도 확인되지 않는다. 그래서 실제 DataSource 를
7
+ * 세운다(멀티DB 규약상 sqlite 로 도는 것이 5개 드라이버 공통의 최소선이기도 하다).
8
+ *
9
+ * ── 이 검사가 지키는 것 ────────────────────────────────────────────────────
10
+ * · 문서의 세 축이 행으로 선다
11
+ * · 설비→자리가 **진짜 FK** 로 이어진다(문자열 대조가 아니라)
12
+ * · 끊어진 참조는 **이름을 대고 보고**되고, 행 자체는 남는다(막지 않되 침묵하지 않는다)
13
+ * · **전량 교체** — 두 번 돌려도 늘지 않고, 원본에서 사라진 것은 행에서도 사라진다
14
+ * · 원본 표식(`sourceRef`·`ingestedAt`) 없는 행은 만들 수 없다(캐시라는 사실이 구조로 지켜진다)
15
+ * · 소요시간은 ISO 8601 **문자열 그대로** 남는다(분으로 접으면 초가 조용히 사라진다)
16
+ */
17
+ import { test, before, after, beforeEach } from 'node:test'
18
+ import assert from 'node:assert/strict'
19
+ import { createRequire } from 'node:module'
20
+ import type { DataSource } from 'typeorm'
21
+
22
+ const req = createRequire(import.meta.url)
23
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
24
+ const { entities: authEntities } = req('@things-factory/auth-base')
25
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
26
+ const { projectStructure } = req('../dist-server/service/twin-model/project-structure.js')
27
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
28
+ const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
29
+ const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
30
+ const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
31
+ const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
32
+ const { TwinOperation } = req('../dist-server/service/twin-model/twin-operation.js')
33
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
34
+
35
+ let ds: DataSource
36
+ let domainId: string
37
+
38
+ const INSTANCE = 'projection-probe'
39
+ const SOURCE = 'projection-probe'
40
+
41
+ /** 실제 트레일러 트윈과 같은 모양의 문서(축소판). */
42
+ const model = () => ({
43
+ locations: [
44
+ { id: 'cut-station', type: 'cut-station', name: '프레임 절단', capacity: 20, parentId: 'a-floor' },
45
+ { id: 'weld-station', type: 'weld-station', capacity: 20, parentId: 'a-floor' },
46
+ { id: 'orphan-bin', type: 'storage', capacity: 5, parentId: 'no-such-area' }
47
+ ],
48
+ equipment: [
49
+ { id: 'cutter-1', kind: 'cutter', name: '절단기 1호', homeLocation: 'cut-station', mtbfMs: 3600000,
50
+ properties: [{ id: 'speed', value: '1.2', dataType: 'xs:double', uom: 'MTS' }] },
51
+ { id: 'welder-1', kind: 'welder', homeLocation: 'weld-station' },
52
+ { id: 'ghost-1', kind: 'welder', homeLocation: 'no-such-location' }
53
+ ],
54
+ operations: [
55
+ { key: 'cut', label: 'twin.tpl.x.cut', intent: 'process', locationType: 'cut-station', resourceType: 'cutter',
56
+ duration: 'PT1H10M30S', variability: { distribution: 'triangular', min: 'PT1H', max: 'PT1H30M' },
57
+ parameters: [{ id: 'yield', value: '0.995' }],
58
+ /* 네 자원 명세 — 설비는 `resourceType`, 나머지 셋은 아래. */
59
+ personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }],
60
+ physicalAssetSpecification: [{ assetClass: 'jig', quantity: 1 }],
61
+ materialSpecification: [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }] }
62
+ ]
63
+ })
64
+
65
+ before(async () => {
66
+ ds = new TypeOrmDataSource({
67
+ type: 'sqlite',
68
+ database: ':memory:',
69
+ synchronize: true,
70
+ dropSchema: true,
71
+ namingStrategy: new NamingStrategy(),
72
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
73
+ logging: false
74
+ })
75
+ await ds.initialize()
76
+ addDataSource('default', ds)
77
+ const d = await ds.getRepository(Domain).save({ name: 'projection-test', subdomain: 'projection-test' } as any)
78
+ domainId = d.id
79
+
80
+ await ds.getRepository(TwinInstance).save({
81
+ domain: { id: domainId },
82
+ instanceId: INSTANCE,
83
+ kind: 'mes',
84
+ realityMode: 'sim-experiment',
85
+ purpose: 'operational',
86
+ status: 'stopped',
87
+ model: {}
88
+ } as any)
89
+
90
+ /* 구역은 model 밖(호스트 개념) — 자리가 FK 로 이걸 가리킨다. */
91
+ const space = await ds.getRepository(TwinSpace).save({ domain: { id: domainId }, spaceId: 'sp-1', name: '공장' } as any)
92
+ await ds.getRepository(TwinArea).save({ domain: { id: domainId }, space: { id: space.id }, areaId: 'a-floor', name: '생산 플로어' } as any)
93
+ })
94
+
95
+ after(async () => {
96
+ await ds?.destroy()
97
+ })
98
+
99
+ beforeEach(async () => {
100
+ await projectStructure(domainId, INSTANCE, model(), SOURCE)
101
+ })
102
+
103
+ test('문서의 세 축이 행으로 선다', async () => {
104
+ assert.equal(await ds.getRepository(TwinLocation).count(), 3)
105
+ assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
106
+ assert.equal(await ds.getRepository(TwinOperation).count(), 1)
107
+ })
108
+
109
+ test('설비→자리가 진짜 FK 로 이어진다 — 문자열 대조가 아니다', async () => {
110
+ const eq = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'cutter-1' }, relations: ['homeLocation'] })
111
+ assert.equal(eq?.homeLocation?.locationId, 'cut-station', 'FK 로 자리를 따라갈 수 있어야 한다')
112
+ /* 자리 쪽에서도 같은 행이다 — 문자열이었으면 같은 값이지만 같은 행은 아니다. */
113
+ const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' } })
114
+ assert.equal(eq?.homeLocation?.id, loc?.id)
115
+ })
116
+
117
+ test('자리→구역이 FK 로 이어진다 — 커널이 "밖" 이라 부른 참조가 호스트에서는 평범한 FK 다', async () => {
118
+ const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' }, relations: ['area'] })
119
+ assert.equal(loc?.area?.areaId, 'a-floor')
120
+ })
121
+
122
+ test('끊어진 참조는 이름을 대고 보고된다 — 막지 않되 침묵하지 않는다', async () => {
123
+ const report = await projectStructure(domainId, INSTANCE, model(), SOURCE)
124
+ assert.deepEqual(report.unresolvedHome, ['ghost-1→no-such-location'])
125
+ assert.deepEqual(report.unresolvedArea, ['orphan-bin→no-such-area'])
126
+
127
+ /* 행 자체는 남는다 — 참조 하나 때문에 설비가 통째로 사라지면 그게 더 나쁜 거짓말이다. */
128
+ const ghost = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'ghost-1' }, relations: ['homeLocation'] })
129
+ assert.ok(ghost, '끊어진 참조를 가진 설비도 행으로 남는다')
130
+ assert.equal(ghost?.homeLocation, null, '이을 수 없는 참조는 비운다(엉뚱한 자리에 붙이지 않는다)')
131
+ })
132
+
133
+ test('전량 교체 — 두 번 돌려도 늘지 않는다', async () => {
134
+ await projectStructure(domainId, INSTANCE, model(), SOURCE)
135
+ await projectStructure(domainId, INSTANCE, model(), SOURCE)
136
+ assert.equal(await ds.getRepository(TwinLocation).count(), 3)
137
+ assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
138
+ })
139
+
140
+ test('원본에서 사라진 것은 행에서도 사라진다 — 캐시는 원본을 따라간다', async () => {
141
+ const shrunk = model()
142
+ shrunk.locations = shrunk.locations.filter(l => l.id !== 'weld-station')
143
+ shrunk.equipment = shrunk.equipment.filter(m => m.id !== 'welder-1')
144
+ await projectStructure(domainId, INSTANCE, shrunk, SOURCE)
145
+ assert.equal(await ds.getRepository(TwinLocation).count(), 2)
146
+ assert.equal(await ds.getRepository(TwinEquipment).count(), 2)
147
+ assert.equal(await ds.getRepository(TwinLocation).countBy({ locationId: 'weld-station' } as any), 0)
148
+ })
149
+
150
+ test('원본 표식 없는 행은 만들 수 없다 — 캐시라는 사실이 구조로 지켜진다', async () => {
151
+ await assert.rejects(
152
+ () =>
153
+ ds.getRepository(TwinLocation).save({
154
+ domain: { id: domainId },
155
+ locationId: 'no-origin',
156
+ type: 'storage'
157
+ /* sourceRef · ingestedAt 없음 */
158
+ } as any),
159
+ /NOT NULL|constraint/i,
160
+ '출처 없는 행이 들어가면 "원본에서 온 캐시" 라는 규율이 구조로는 안 지켜지는 것이다'
161
+ )
162
+ })
163
+
164
+ test('소요시간은 ISO 8601 문자열 그대로 — 분으로 접으면 초가 조용히 사라진다', async () => {
165
+ const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
166
+ assert.equal(op?.duration, 'PT1H10M30S')
167
+ /* 변동성·파라미터는 도메인마다 형이 달라 원형 그대로 싣는다. */
168
+ assert.equal(op?.variability?.distribution, 'triangular')
169
+ assert.deepEqual(op?.parameters, [{ id: 'yield', value: '0.995' }])
170
+ })
171
+
172
+ test('등록되지 않은 트윈에는 투영하지 않는다 — 주인 없는 행을 만들지 않는다', async () => {
173
+ await assert.rejects(() => projectStructure(domainId, 'not-registered', model(), SOURCE), /not registered/)
174
+ })
175
+
176
+ test('네 자원 명세가 함께 옮겨진다 — 선언한 것만 제약이 되므로 빠지면 화면이 거짓이 된다', async () => {
177
+ /*
178
+ * ISA-95 는 공정이 무엇을 필요로 하는지를 넷으로 말한다: 설비·인원·자산·자재. 셋이 투영에서
179
+ * 떨어져 있어서, 조회 화면이 "원본이 말한 값" 이라 적고도 넷 중 하나만 보였다 — 사람이 필요한
180
+ * 공정을 보고 **사람이 필요 없다고 결론 내게** 되어 있었다.
181
+ */
182
+ const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
183
+ assert.deepEqual(op?.personnelSpecification, [{ personnelClass: 'cutter-op', quantity: 1 }], '인원 명세가 행에 남아야 한다')
184
+ assert.deepEqual(op?.physicalAssetSpecification, [{ assetClass: 'jig', quantity: 1 }], '자산 명세도 남아야 한다')
185
+ assert.deepEqual(op?.materialSpecification, [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }], '자재 명세도 남아야 한다')
186
+ })
187
+
188
+ test('이름이 행까지 온다 — 이름이 없으면 화면에 id 만 남는다', async () => {
189
+ /*
190
+ * 투영이 마스터의 `name` 을 버리고 있었다. 행에 이름이 없으면 어떤 화면도 이름을 만들 수 없어서
191
+ * 목록·표·관계 그림이 모두 `cut-station` 을 찍었다 — 표면마다 따로 보이지만 결함은 여기 하나다.
192
+ */
193
+ const loc = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'cut-station' } as any })
194
+ assert.equal(loc!.name, '프레임 절단', '자리의 이름이 행에 실려야 한다')
195
+
196
+ const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
197
+ assert.equal(eq!.name, '절단기 1호', '설비의 이름이 행에 실려야 한다')
198
+
199
+ /* 공정은 마스터가 `label` 로 말한다 — 행에서는 규약의 이름으로 둔다(어휘를 하나로). */
200
+ const op = await ds.getRepository(TwinOperation).findOne({ where: { domain: { id: domainId } as any, operationKey: 'cut' } as any })
201
+ assert.equal(op!.name, 'twin.tpl.x.cut', '공정의 이름이 행에 실려야 한다')
202
+
203
+ /* **이름 없는 원본은 이름 없이 남는다** — id 를 이름으로 베끼면 "이름이 있다" 는 거짓이 되고,
204
+ 화면은 이름 없음을 알아볼 수 없게 된다. */
205
+ const noname = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'weld-station' } as any })
206
+ assert.equal(noname!.name ?? null, null, '원본이 이름을 말하지 않았으면 비어 있어야 한다')
207
+ })
208
+
209
+ test('자원 속성이 행까지 온다 — 소비처가 실제로 읽는 사실이다(주행 속도)', async () => {
210
+ /*
211
+ * 모델에는 있는데 행에 자리가 없어 투영이 흘리고 있었다. 그러면 조회 화면은 "원본이 말한 값" 이라
212
+ * 적고도 속성을 못 보인다 — 이름이 같은 식으로 사라졌던 것과 같은 부류다.
213
+ *
214
+ * 단위를 값과 함께 담는지도 본다: 단위 없는 "1.2" 는 초속인지 시속인지 말하지 않는다.
215
+ */
216
+ const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
217
+ const props = (eq!.properties ?? []) as any[]
218
+ assert.equal(props.length, 1, '선언한 속성이 행에 실려야 한다')
219
+ assert.equal(props[0].id, 'speed')
220
+ assert.equal(props[0].value, '1.2')
221
+ assert.equal(props[0].uom, 'MTS', '단위가 값과 함께 남아야 한다')
222
+
223
+ /* 선언하지 않은 자원은 **비어 있다** — 빈 배열로 "선언했다" 고 말하지 않는다. */
224
+ const noneProps = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'welder-1' } as any })
225
+ assert.equal(noneProps!.properties ?? null, null)
226
+ })