@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,122 @@
1
+ /*
2
+ * 뮤테이션 게이트 가드 — **트윈을 바꾸는 문은 전부 잠겨 있다.**
3
+ *
4
+ * ── 계기 (2026-08-06) ──────────────────────────────────────────────────────
5
+ * 권한 모델을 손대며 세어 보니 트윈 뮤테이션 24개 중 **22개에 게이트가 없었다.** 인스턴스 삭제와
6
+ * 레퍼런스 자격정보 저장까지 로그인만 하면 누구나 부를 수 있었다. 발견은 사람 눈이었다 — 즉
7
+ * 25번째 뮤테이션이 게이트 없이 들어오면 아무것도 잡지 못한다. 그래서 기계가 센다.
8
+ *
9
+ * ── 왜 문자열을 그대로 비교하나 ─────────────────────────────────────────────
10
+ * "게이트가 있나" 만 보면 `domainOwnerGranted: false` 로 약해지거나 category 가 다른 값으로 갈라져도
11
+ * 통과한다. 게이트는 **한 문장이어야** 서버·클라·관리 화면이 같은 권한을 말한다. 그래서 정본 문장과
12
+ * 글자까지 같은지 본다.
13
+ *
14
+ * ── 조회는 왜 검사하지 않나 (ADR-0027) ──────────────────────────────────────
15
+ * 조회에는 게이트를 두지 않기로 했다 — `twinKpi` 가 같은 판정을 게이트 없이 이미 내주므로, 일부
16
+ * 조회만 잠그면 보안이 아니라 화면 고장이 된다. 이 결정이 바뀌면 아래 "정본 문장" 검사가 먼저
17
+ * 깨진다. 그때는 테스트를 고치지 말고 **ADR 을 먼저 고친다.**
18
+ */
19
+ import { test } from 'node:test'
20
+ import assert from 'node:assert/strict'
21
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
22
+ import { join } from 'node:path'
23
+
24
+ /** 정본 게이트 — 트윈을 **바꾸는** 모든 작업이 요구하는 단 하나의 권한. */
25
+ const GATE =
26
+ `@Directive('@privilege(category: "twin", privilege: "mutation", ` + `domainOwnerGranted: true, superUserGranted: true)')`
27
+
28
+ /**
29
+ * 두 번째 게이트 — **테넌트를 넘어 읽는 운영 계기판** (ADR-0027 예외, 2026-08-07).
30
+ *
31
+ * ADR-0027 의 전제는 "읽기의 경계는 테넌트 소속" 이라 조회에는 게이트를 붙이지 않는다. 그런데 프로세스
32
+ * 부하 계기판(`twinLoad`)은 이벤트 루프가 프로세스 공유이므로 **일부러 전 테넌트를 합산한다** — 소속이
33
+ * 경계가 되지 못한다. 그래서 시스템 소유만 본다. **권한 이름을 만들지 않았다**(category·privilege 없음).
34
+ *
35
+ * 늘리지 말 것. 조건은 ADR-0027 의 예외 절에 있고, 셋 중 하나라도 아니면 조회 무게이트가 그대로다.
36
+ */
37
+ const OPS_GATE = `@Directive('@privilege(superUserGranted: true)')`
38
+
39
+ const ALLOWED = [GATE, OPS_GATE]
40
+
41
+ const SERVER = new URL('../server', import.meta.url).pathname
42
+
43
+ function walk(dir: string, out: string[] = []): string[] {
44
+ let entries: string[]
45
+ try {
46
+ entries = readdirSync(dir)
47
+ } catch {
48
+ return out
49
+ }
50
+ for (const e of entries) {
51
+ const p = join(dir, e)
52
+ if (statSync(p).isDirectory()) walk(p, out)
53
+ else if (p.endsWith('.ts') && !p.endsWith('.d.ts')) out.push(p)
54
+ }
55
+ return out
56
+ }
57
+
58
+ /**
59
+ * `@Mutation` 위에 붙은 데코레이터·주석 덩이를 거꾸로 모은다.
60
+ *
61
+ * 데코레이터가 바로 붙어 있다고 가정하지 않는다 — 나중에 다른 데코레이터가 끼어들어도 게이트를
62
+ * 찾아야 한다. 덩이의 끝은 코드(다른 메서드의 `}` 등)를 만나는 자리다.
63
+ */
64
+ function decoratorBlockAbove(lines: string[], at: number): string[] {
65
+ const block: string[] = []
66
+ for (let i = at - 1; i >= 0; i--) {
67
+ const t = lines[i].trim()
68
+ if (!t) continue /* 주석과 데코레이터 사이의 빈 줄은 덩이를 끊지 않는다 */
69
+ const isBlockPart = t.startsWith('@') || t.startsWith('*') || t.startsWith('/*') || t.startsWith('//')
70
+ if (!isBlockPart) break
71
+ block.push(t)
72
+ }
73
+ return block
74
+ }
75
+
76
+ const files = walk(SERVER)
77
+
78
+ test('트윈을 바꾸는 뮤테이션은 전부 정본 게이트를 달고 있다', () => {
79
+ const ungated: string[] = []
80
+ let mutations = 0
81
+
82
+ for (const f of files) {
83
+ const lines = readFileSync(f, 'utf-8').split('\n')
84
+ lines.forEach((line, i) => {
85
+ if (!line.trim().startsWith('@Mutation')) return
86
+ mutations++
87
+ if (!decoratorBlockAbove(lines, i).some(l => l === GATE)) {
88
+ /* 어느 뮤테이션인지 이름으로 말한다 — 줄번호만 주면 고치는 사람이 다시 찾아야 한다. */
89
+ const name = (lines.slice(i, i + 6).join(' ').match(/async\s+(\w+)/) ?? [, '?'])[1]
90
+ ungated.push(`${f.split('/').slice(-2).join('/')}:${i + 1} ${name}`)
91
+ }
92
+ })
93
+ }
94
+
95
+ /* 훑기가 망가져 0건이 되면 "위반 없음" 으로 보인다 — 그 거짓 통과를 막는다. */
96
+ assert.ok(mutations >= 20, `뮤테이션을 ${mutations}개만 찾았다 — 소스 훑기가 망가졌다`)
97
+ assert.deepEqual(
98
+ ungated,
99
+ [],
100
+ `게이트 없는 뮤테이션 ${ungated.length}건 — 로그인만 하면 누구나 부를 수 있다:\n${ungated.join('\n')}`
101
+ )
102
+ })
103
+
104
+ test('권한 문장이 하나로 통일되어 있다 — 약해지거나 서로 어긋나지 않았다', () => {
105
+ const odd: string[] = []
106
+ for (const f of files) {
107
+ readFileSync(f, 'utf-8')
108
+ .split('\n')
109
+ .forEach((line, i) => {
110
+ const t = line.trim()
111
+ if (!t.startsWith("@Directive('@privilege")) return
112
+ if (!ALLOWED.includes(t)) odd.push(`${f.split('/').slice(-2).join('/')}:${i + 1} ${t}`)
113
+ })
114
+ }
115
+ assert.deepEqual(
116
+ odd,
117
+ [],
118
+ `정본과 다른 권한 문장 ${odd.length}건. 쓰기는 GATE 하나, 테넌트를 넘어 읽는 운영 계기판만 ` +
119
+ `OPS_GATE. 그 밖으로 늘리는 것이 의도라면 ADR-0027 을 먼저 고치고 이 검사의 정본을 함께 옮긴다:` +
120
+ `\n${odd.join('\n')}`
121
+ )
122
+ })
@@ -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
 
@@ -94,9 +94,92 @@ test('★ withLiveOee: projector 미러(oee 없음) + 누적기 → live 설비
94
94
 
95
95
  // withLiveOee 로 보강 → buildEntityDeltas payload 에 oee 포함(sim 과 동일 형상 = 보드 OEE 컴포넌트 live 렌더).
96
96
  const enriched = withLiveOee(rawLive, acc)
97
- const fk = buildEntityDeltas(enriched).find(d => d.tag === 'fk-1')!.data as any
97
+ const fk = buildEntityDeltas(enriched, 't1').find(d => d.tag === 'fk-1')!.data as any
98
98
  assert.ok(fk.oee, 'withLiveOee 후 설비 data(tag) payload 에 oee 포함 — live OEE 갭 종료')
99
99
  // buildEntityDeltas shapeOee = 정수 퍼센트(0-100), raw ms 제외.
100
100
  assert.ok(fk.oee.overall >= 0 && fk.oee.overall <= 100, 'oee.overall ∈ [0,100] 정수 퍼센트')
101
101
  assert.ok(fk.oee.availability < 100, 'availability < 100 (고장 시간 반영 — 누적기 downMs)')
102
102
  })
103
+
104
+ /*
105
+ * ── 되짚은 화면에도 가동 이력이 있어야 한다 (2026-08-05) ────────────────────
106
+ * OEE 는 원 시스템이 누적을 보내 주지 않아 호스트가 상태 전이를 적분해 만든다. 그런데 그 누적기는
107
+ * **라이브에서만** 돌았고, 저널을 접는 경로는 그 계산을 하지 않았다 — 과거를 되짚으면 모든 설비가
108
+ * "가동 이력이 전혀 없음" 으로 보였다(실 저널: `rosarito-mes` 18대, `hatiolab-wms` 22대가 그랬다).
109
+ *
110
+ * 없는 것은 데이터가 아니라 계산이다: 입력은 저널에 다 있다. 그래서 같은 누적기에 같은 이벤트를
111
+ * 태운다 — 이 테스트는 그 재계산이 라이브와 **같은 값**을 내는지, 그리고 시간여행에서 **그 시점까지만**
112
+ * 세는지를 못 박는다.
113
+ */
114
+ test('★ 저널을 다시 태우면 라이브와 같은 계측이 나온다 — 규칙이 하나이므로', () => {
115
+ const T0 = Date.parse('2026-08-05T00:00:00.000Z')
116
+ const ev = (ms: number, id: string, status: string) => ({
117
+ eventId: `e${ms}`,
118
+ eventType: 'equipment.status',
119
+ eventTime: new Date(T0 + ms).toISOString(),
120
+ tenantId: 't',
121
+ data: { equipmentId: id, moverId: id, status }
122
+ })
123
+ const stream = [ev(0, 'fk-1', 'idle'), ev(10_000, 'fk-1', 'busy'), ev(40_000, 'fk-1', 'down'), ev(70_000, 'fk-1', 'idle')]
124
+
125
+ /* 라이브 — 이벤트가 도착하는 대로 누적. */
126
+ const live = new OeeAccumulator()
127
+ for (const e of stream) live.apply(e as any)
128
+
129
+ /* 되짚기 — 저널에서 같은 순서로 다시 태운다. */
130
+ const replayed = new OeeAccumulator()
131
+ for (const e of stream) replayed.apply(e as any)
132
+
133
+ const at = T0 + 70_000
134
+ assert.deepEqual(replayed.oee('fk-1', at), live.oee('fk-1', at), '같은 이벤트·같은 시점이면 같은 값이어야 한다')
135
+ assert.equal(replayed.oee('fk-1', at).downMs, 30_000, '고장 30초가 잡혀야 한다')
136
+ })
137
+
138
+ test('★ 시간여행은 그 시점까지만 센다 — 뒤에 일어난 고장이 과거 화면에 섞이지 않게', () => {
139
+ const T0 = Date.parse('2026-08-05T00:00:00.000Z')
140
+ const ev = (ms: number, status: string) => ({
141
+ eventId: `e${ms}`,
142
+ eventType: 'equipment.status',
143
+ eventTime: new Date(T0 + ms).toISOString(),
144
+ tenantId: 't',
145
+ data: { equipmentId: 'fk-1', moverId: 'fk-1', status }
146
+ })
147
+ const all = [ev(0, 'busy'), ev(30_000, 'down'), ev(60_000, 'idle')]
148
+
149
+ /* 30초 시점까지만 태운다(호스트가 잘라 낸 이벤트만 태우는 것과 같다). */
150
+ const past = new OeeAccumulator()
151
+ for (const e of all.slice(0, 2)) past.apply(e as any)
152
+ assert.equal(past.oee('fk-1', T0 + 30_000).downMs, 0, '아직 고장이 시작된 시점 — 고장 시간은 0 이다')
153
+
154
+ const now = new OeeAccumulator()
155
+ for (const e of all) now.apply(e as any)
156
+ assert.equal(now.oee('fk-1', T0 + 60_000).downMs, 30_000, '지금 화면에서는 고장 30초가 잡힌다')
157
+ })
158
+
159
+ test('★ 계측 시점을 벽시계로 재지 않는다 — 과거 화면의 가용률이 시간이 갈수록 떨어지지 않게', () => {
160
+ const T0 = Date.parse('2026-08-05T00:00:00.000Z')
161
+ const acc = new OeeAccumulator()
162
+ acc.apply({ eventId: 'e1', eventType: 'equipment.status', eventTime: new Date(T0).toISOString(), tenantId: 't', data: { equipmentId: 'fk-1', moverId: 'fk-1', status: 'busy' } } as any)
163
+ const atThen = acc.oee('fk-1', T0 + 10_000)
164
+ const atMuchLater = acc.oee('fk-1', T0 + 10_000_000)
165
+ assert.notDeepEqual(atThen, atMuchLater, '시점을 넘기면 값이 달라진다 — 그래서 그 화면의 시각을 넘겨야 한다')
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
+ })