@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
@@ -0,0 +1,223 @@
1
+ /*
2
+ * 공간 ↔ 트윈 ↔ 가시화 정합성 — 어긋난 모양마다 하나씩 세운다.
3
+ *
4
+ * 검사가 **무는지** 확인하는 것이 이 파일의 일이다: 정합한 입력에서는 아무것도 내지 않고, 어긋난
5
+ * 입력에서는 그 자리를 코드와 id 로 지목해야 한다. 지목하지 못하면 사람이 어디를 볼지 알 수 없다.
6
+ */
7
+ import { test } from 'node:test'
8
+ import assert from 'node:assert/strict'
9
+
10
+ import { checkSpaceIntegrity, sortFindings, type SpaceIntegrityInput } from '../server/service/twin-space/space-integrity.ts'
11
+
12
+ /** 정합한 한 현장 — 트윈 하나(창고), 지도 표현 하나, 구역 하나, 폴리곤이 그 구역과 로케이션을 가리킨다. */
13
+ const SOUND: SpaceIntegrityInput = {
14
+ spaces: [{ spaceId: 'rosarito-site', primaryRepresentationId: 'rep-map' }],
15
+ instances: [
16
+ {
17
+ instanceId: 'wms-a',
18
+ spaceId: 'rosarito-site',
19
+ modelSpaceId: 'rosarito-site',
20
+ locationIds: ['rack-1', 'dock-1'],
21
+ equipmentIds: ['fork-1'],
22
+ areaRefs: ['bldg-1']
23
+ }
24
+ ],
25
+ representations: [
26
+ {
27
+ id: 'rep-map',
28
+ spaceId: 'rosarito-site',
29
+ type: 'map',
30
+ polygons: [
31
+ { id: 'p1', groupId: 'bldg-1' },
32
+ { id: 'p2', targetKind: 'location', targetId: 'rack-1' },
33
+ { id: 'p3', targetKind: 'equipment', targetId: 'fork-1' }
34
+ ]
35
+ }
36
+ ],
37
+ areasBySpace: { 'rosarito-site': ['bldg-1'] },
38
+ boardIds: []
39
+ }
40
+
41
+ test('정합하면 아무것도 내지 않는다 — 멀쩡한 현장을 시끄럽게 만들지 않는다', () => {
42
+ assert.deepEqual(checkSpaceIntegrity(SOUND), [])
43
+ })
44
+
45
+ test('없는 현장을 가리키는 트윈을 잡는다 — 무대가 사라진 트윈', () => {
46
+ const f = checkSpaceIntegrity({
47
+ ...SOUND,
48
+ instances: [{ ...SOUND.instances[0], spaceId: 'gone-site', modelSpaceId: 'gone-site' }]
49
+ })
50
+ const hit = f.find(x => x.code === 'twin-space-missing')
51
+ assert.ok(hit)
52
+ assert.equal(hit.severity, 'broken')
53
+ assert.equal(hit.instanceId, 'wms-a')
54
+ assert.equal(hit.spaceId, 'gone-site')
55
+ })
56
+
57
+ test('레지스트리와 model 의 현장이 다르면 잡는다 — 어느 쪽이 사실인지 알 수 없다', () => {
58
+ const f = checkSpaceIntegrity({
59
+ ...SOUND,
60
+ instances: [{ ...SOUND.instances[0], modelSpaceId: 'other-site' }]
61
+ })
62
+ const hit = f.find(x => x.code === 'twin-space-mismatch')
63
+ assert.ok(hit)
64
+ assert.equal(hit.severity, 'broken')
65
+ assert.equal(hit.params?.registry, 'rosarito-site')
66
+ assert.equal(hit.params?.model, 'other-site')
67
+ })
68
+
69
+ test('현장 없는 트윈은 경고로 낸다 — 뗀 결과일 수 있어 고장은 아니다', () => {
70
+ const f = checkSpaceIntegrity({
71
+ ...SOUND,
72
+ instances: [{ ...SOUND.instances[0], spaceId: undefined, modelSpaceId: undefined }]
73
+ })
74
+ const hit = f.find(x => x.code === 'twin-without-space')
75
+ assert.ok(hit)
76
+ assert.equal(hit.severity, 'warn')
77
+ })
78
+
79
+ test('로케이션이 이 현장에 없는 구역에 소속되어 있으면 잡는다', () => {
80
+ const f = checkSpaceIntegrity({
81
+ ...SOUND,
82
+ instances: [{ ...SOUND.instances[0], areaRefs: ['bldg-1', 'floor-9', 'floor-9'] }]
83
+ })
84
+ const hit = f.find(x => x.code === 'location-area-missing')
85
+ assert.ok(hit)
86
+ assert.equal(hit.params?.count, 1, '중복은 한 번만 센다')
87
+ assert.equal(hit.params?.areas, 'floor-9')
88
+ })
89
+
90
+ test('폴리곤이 없는 구역을 가리키면 잡는다 — 그 도형은 아무것도 보여 주지 않는다', () => {
91
+ const f = checkSpaceIntegrity({
92
+ ...SOUND,
93
+ representations: [{ ...SOUND.representations[0], polygons: [{ id: 'p1', groupId: 'ghost-area' }] }]
94
+ })
95
+ const hit = f.find(x => x.code === 'polygon-area-missing')
96
+ assert.ok(hit)
97
+ assert.equal(hit.severity, 'broken')
98
+ assert.equal(hit.representationId, 'rep-map')
99
+ assert.equal(hit.params?.areaId, 'ghost-area')
100
+ })
101
+
102
+ test('폴리곤이 이 현장의 어느 트윈에도 없는 로케이션을 가리키면 잡는다 — 트윈을 옮긴 뒤 남는 모양', () => {
103
+ const f = checkSpaceIntegrity({
104
+ ...SOUND,
105
+ instances: [{ ...SOUND.instances[0], spaceId: 'other-site', modelSpaceId: 'other-site' }],
106
+ spaces: [...SOUND.spaces, { spaceId: 'other-site' }]
107
+ })
108
+ const hits = f.filter(x => x.code === 'polygon-target-missing')
109
+ assert.equal(hits.length, 2, '로케이션과 설비 둘 다 끊긴다')
110
+ assert.equal(hits[0].severity, 'broken')
111
+ assert.equal(hits.find(h => h.params?.kind === 'equipment')?.params?.targetId, 'fork-1')
112
+ })
113
+
114
+ test('설비 바인딩을 로케이션 목록으로 판정하지 않는다 — 종류를 섞으면 거짓 경보가 난다', () => {
115
+ const f = checkSpaceIntegrity({
116
+ ...SOUND,
117
+ representations: [
118
+ { ...SOUND.representations[0], polygons: [{ id: 'p3', targetKind: 'equipment', targetId: 'rack-1' }] }
119
+ ]
120
+ })
121
+ assert.ok(f.find(x => x.code === 'polygon-target-missing'), '로케이션 id 를 설비로 가리키면 끊긴 것이다')
122
+ })
123
+
124
+ test('대표 표현이 이 현장의 것이 아니면 잡는다', () => {
125
+ const f = checkSpaceIntegrity({
126
+ ...SOUND,
127
+ spaces: [{ spaceId: 'rosarito-site', primaryRepresentationId: 'rep-elsewhere' }]
128
+ })
129
+ const hit = f.find(x => x.code === 'primary-representation-missing')
130
+ assert.ok(hit)
131
+ assert.equal(hit.severity, 'broken')
132
+ })
133
+
134
+ test('보드 표현이 보드를 가리키지 않으면 잡는다', () => {
135
+ const f = checkSpaceIntegrity({
136
+ ...SOUND,
137
+ representations: [{ id: 'rep-board', spaceId: 'rosarito-site', type: 'board', boardId: null, polygons: [] }],
138
+ spaces: [{ spaceId: 'rosarito-site', primaryRepresentationId: 'rep-board' }]
139
+ })
140
+ const hit = f.find(x => x.code === 'board-representation-unbound')
141
+ assert.ok(hit)
142
+ assert.equal(hit.params?.boardId, '—')
143
+ })
144
+
145
+ test('없는 보드를 가리키는 보드 표현도 잡는다', () => {
146
+ const f = checkSpaceIntegrity({
147
+ ...SOUND,
148
+ representations: [{ id: 'rep-board', spaceId: 'rosarito-site', type: 'board', boardId: 'gone', polygons: [] }],
149
+ spaces: [{ spaceId: 'rosarito-site', primaryRepresentationId: 'rep-board' }],
150
+ boardIds: ['other-board']
151
+ })
152
+ assert.ok(f.find(x => x.code === 'board-representation-unbound'))
153
+ })
154
+
155
+ test('트윈이 사는데 표현이 없으면 경고한다 — 볼 화면이 없다', () => {
156
+ const f = checkSpaceIntegrity({ ...SOUND, representations: [], spaces: [{ spaceId: 'rosarito-site' }] })
157
+ const hit = f.find(x => x.code === 'space-without-representation')
158
+ assert.ok(hit)
159
+ assert.equal(hit.params?.twins, 1)
160
+ })
161
+
162
+ test('트윈이 없는 빈 현장은 정상이다 — 만들어 두고 아직 안 쓴 현장', () => {
163
+ const f = checkSpaceIntegrity({
164
+ spaces: [{ spaceId: 'empty-site' }],
165
+ instances: [],
166
+ representations: [],
167
+ areasBySpace: {},
168
+ boardIds: []
169
+ })
170
+ assert.deepEqual(f, [])
171
+ })
172
+
173
+ test('벤치 사본은 검사 대상이 아니다 — 잠깐 살고 지워지는 사본', () => {
174
+ const f = checkSpaceIntegrity({
175
+ ...SOUND,
176
+ instances: [
177
+ ...SOUND.instances,
178
+ { instanceId: 'bench-1', spaceId: 'nowhere-site', modelSpaceId: 'else', purpose: 'bench' }
179
+ ]
180
+ })
181
+ assert.deepEqual(f, [], '사본의 어긋남은 세지 않는다')
182
+ })
183
+
184
+ test('심각한 것이 먼저 온다 — 사람이 무엇부터 볼지 정하지 않아도 되게', () => {
185
+ const f = sortFindings([
186
+ { code: 'twin-without-space', severity: 'warn', message: 'w' },
187
+ { code: 'twin-space-missing', severity: 'broken', message: 'b' }
188
+ ])
189
+ assert.equal(f[0].severity, 'broken')
190
+ })
191
+
192
+ /*
193
+ * ── 어휘가 맞지 않는 트윈 (2026-08-14, EMS 를 붙이며) ────────────────────────
194
+ *
195
+ * 커널은 모르는 `eventType` 을 버리고 종류별로 센다(`state.unhandled`). 그 수가 0이 아니면 저널은
196
+ * 들어오는데 상태가 채워지지 않는 것이고, 화면에는 「정상인데 빈」 트윈으로 보인다.
197
+ *
198
+ * 이 검사가 지키는 것은 **모른다와 없다의 구별**이다: 정지한 트윈은 확인한 바가 없으므로 세지 않는다.
199
+ */
200
+ test('담을 줄 모르는 어휘가 들어오는 트윈을 잡는다 — 리비전만 오르는 빈 트윈', () => {
201
+ const f = checkSpaceIntegrity({
202
+ ...SOUND,
203
+ instances: [{ ...SOUND.instances[0], unhandled: [{ eventType: 'energy.measured', count: 96 }, { eventType: 'energy.peak', count: 4 }] }]
204
+ })
205
+ const hit = f.find(x => x.code === 'twin-unhandled-vocabulary')!
206
+ assert.ok(hit, '이것을 놓치면 사용자는 커넥터가 잘못 붙은 것을 알 수 없다')
207
+ assert.equal(hit.severity, 'warn')
208
+ assert.equal(hit.instanceId, 'wms-a', '어느 트윈인지 지목해야 한다')
209
+ assert.equal(hit.params?.events, 100, '건수를 합쳐 낸다')
210
+ assert.equal(hit.params?.kinds, 2)
211
+ assert.match(String(hit.params?.types), /energy\.measured/, '많은 것부터 이름을 낸다')
212
+ })
213
+
214
+ test('정지한 트윈은 세지 않는다 — 확인하지 않은 것을 정합으로도 결함으로도 세지 않는다', () => {
215
+ /* `unhandled` 가 undefined 인 것은 「없다」가 아니라 「모른다」다(런타임이 없으면 알 수 없다). */
216
+ const f = checkSpaceIntegrity(SOUND)
217
+ assert.equal(f.find(x => x.code === 'twin-unhandled-vocabulary'), undefined)
218
+ })
219
+
220
+ test('빈 배열은 결함이 아니다 — 받은 것이 없으면 조용하다', () => {
221
+ const f = checkSpaceIntegrity({ ...SOUND, instances: [{ ...SOUND.instances[0], unhandled: [] }] })
222
+ assert.equal(f.find(x => x.code === 'twin-unhandled-vocabulary'), undefined)
223
+ })
@@ -0,0 +1,94 @@
1
+ /*
2
+ * 적합성 패널이 **그 트윈이 기대는 표준들**을 낸다 — 하나만 말하지 않는다.
3
+ *
4
+ * ── 무엇이 부족했나 (2026-08-15) ────────────────────────────────────────────
5
+ * 패널이 트윈 종류와 무관하게 ISA-95 하나만 말했다. 에너지 트윈에서 그것은 거짓은 아니지만
6
+ * (설비·계층·속성은 에너지에서도 ISA-95 자원이다) **부족하다**: 계량기·차단기·태양광·축전지의 어휘를
7
+ * 주는 것은 IEC 61850 이고, 무엇이 비었는지도 그 목록으로만 셀 수 있다.
8
+ *
9
+ * 그리고 주장 수준을 못 박는다 — 우리가 재는 것은 **어휘와 모델**이지 프로토콜 구현이 아니다.
10
+ */
11
+ import { test } from 'node:test'
12
+ import assert from 'node:assert/strict'
13
+
14
+ import { standardsForKind } from '../server/service/twin-model/standard-coverage.ts'
15
+ import { iec61850Coverage } from '../server/service/twin-model/iec61850-coverage.ts'
16
+ import { epcisCoverage } from '../server/service/twin-model/epcis-coverage.ts'
17
+ import { readFileSync } from 'node:fs'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ const query = readFileSync(fileURLToPath(new URL('../server/service/twin-model/twin-model-query.ts', import.meta.url)), 'utf-8')
21
+
22
+ const AXES = ['equipment', 'demandWindows', 'locations', 'persons', 'assets', 'orders', 'tasks', 'operations']
23
+
24
+ test('ISA-95 는 종류와 상관없이 낸다 — 자원과 계층은 어느 도메인에서나 그 표준의 것이다', () => {
25
+ for (const kind of ['wms', 'yms', 'mes', 'ems']) {
26
+ assert.equal(standardsForKind(kind)[0], 'isa95', `${kind}: ISA-95 가 먼저다`)
27
+ }
28
+ })
29
+
30
+ test('에너지 트윈은 IEC 61850 을 함께 낸다 — 다른 종류에는 붙지 않는다', () => {
31
+ assert.deepEqual(standardsForKind('ems'), ['isa95', 'iec61850'])
32
+ for (const kind of ['wms', 'yms', 'mes']) {
33
+ assert.deepEqual(standardsForKind(kind), ['isa95', 'epcis'], `${kind}: 물류·제조는 EPCIS 다`)
34
+ }
35
+ /* 에너지에 EPCIS 를 붙이지 않는다 — 오가는 물건이 없고, 계측은 사건이 아니라 표본으로 들어온다. */
36
+ assert.equal(standardsForKind('ems').includes('epcis' as any), false)
37
+ assert.deepEqual(standardsForKind(undefined), ['isa95'], '종류를 모르면 공통 표준만')
38
+ })
39
+
40
+ test('질의가 그 정책으로 조립한다 — 규칙이 두 벌이 되지 않게', () => {
41
+ assert.match(query, /standardsForKind\(inst\.kind\)/, '질의가 정책 함수를 써야 한다')
42
+ assert.match(query, /COVERAGE\[id\]/, '표는 id 로 찾는다(종류 분기를 다시 쓰지 않는다)')
43
+ })
44
+
45
+ test('주장 수준을 이름에 적는다 — 프로토콜 적합성으로 읽히지 않게', () => {
46
+ const iec = iec61850Coverage(AXES)
47
+ assert.match(iec.model, /vocabulary/, '어휘를 쓴다는 뜻이지 MMS·GOOSE 를 구현했다는 뜻이 아니다')
48
+ })
49
+
50
+ test('개폐·발전·저장이 세 층 모두 올라갔다 — 값이 먼저, 컴포넌트는 그 다음', () => {
51
+ const iec = iec61850Coverage(AXES)
52
+ const by = Object.fromEntries(iec.concepts.map(c => [c.std, c]))
53
+ /*
54
+ * 오래 「구조만 있음」이던 셋이다. 능력이 상태 필드를 선언해 두었는데 값을 받는 문이 없었다 —
55
+ * 이제 커널이 `energy.equipment` 로 받아 설비에 적고, 씬 컴포넌트가 그린다.
56
+ */
57
+ for (const std of ['XCBR', 'DPVA', 'ZBAT']) {
58
+ assert.equal(by[std].behavior, 'full', `${std}: 값을 채우는 곳이 있다`)
59
+ assert.equal(by[std].surface, 'full', `${std}: 그리는 컴포넌트가 있다`)
60
+ assert.ok(by[std].note, `${std}: 근거를 한 줄로 적어야 한다`)
61
+ }
62
+ /* 채운 수는 `full` 만 센다 — 부분을 채운 것으로 세면 기준이 스스로를 속인다. */
63
+ assert.equal(iec.totals.behavior, 5, '다섯 논리 노드가 모두 실제로 돈다')
64
+ })
65
+
66
+ test('커널 축이 사라지면 구조가 없는 것으로 떨어진다 — 오타·개명이 조용히 통과하지 않게', () => {
67
+ const iec = iec61850Coverage(['equipment']) // demandWindows 축이 없는 세상
68
+ const mmxu = iec.concepts.find(c => c.std === 'MMXU')!
69
+ assert.equal(mmxu.structure, 'none')
70
+ assert.equal((mmxu as any).axisMissing, true)
71
+ })
72
+
73
+ test('EPCIS 표는 실제로 쓰는 것만 「있음」으로 센다 — 모양만 있는 필드는 구조만', () => {
74
+ const epcis = epcisCoverage(['items', 'orders', 'locations', 'tasks', 'operations'])
75
+ const by = Object.fromEntries(epcis.concepts.map(c => [c.std, c]))
76
+ /* 커널이 실제로 만들어 내보내는 사건들. */
77
+ for (const std of ['ObjectEvent', 'AggregationEvent', 'TransactionEvent', 'TransformationEvent']) {
78
+ assert.equal(by[std].behavior, 'full', `${std}: 커널이 실제로 낸다`)
79
+ }
80
+ /* 타입조차 없는 것은 없다고 적는다(EPCIS 2.0 이 더한 사건). */
81
+ assert.equal(by['AssociationEvent'].structure, 'none')
82
+ /* 모양만 있고 아무도 채우지 않는 것들 — 「구조만 있음」이지 「있음」이 아니다. */
83
+ for (const std of ['sourceList / destinationList', 'persistentDisposition', 'sensorElementList']) {
84
+ assert.equal(by[std].structure, 'partial', `${std}: 모양은 있다`)
85
+ assert.equal(by[std].behavior, 'none', `${std}: 아무도 채우지 않는다`)
86
+ assert.ok(by[std].note, `${std}: 왜 그 수준인지 적어야 한다`)
87
+ }
88
+ })
89
+
90
+ test('계측은 EPCIS 센서 필드로 오지 않는다 — 그 사실을 표가 말한다', () => {
91
+ const sensor = epcisCoverage(['items']).concepts.find(c => c.std === 'sensorElementList')!
92
+ assert.equal(sensor.behavior, 'none')
93
+ assert.ok(sensor.note, '에너지가 다른 층으로 들어온다는 사실을 근거로 적는다')
94
+ })
@@ -32,7 +32,7 @@ test('고른 축만 남는다 — 지도가 쓰는 둘', () => {
32
32
  assert.equal(state.items, undefined, '쓰지 않는 축은 오지 않는다')
33
33
  })
34
34
 
35
- test('축을 말하지 않으면 자르지 않는다 — 기존 호출부가 그대로 동작해야 한다', () => {
35
+ test('축을 지정하지 않으면 자르지 않는다 — 기존 호출부가 그대로 동작해야 한다', () => {
36
36
  assert.equal(pickAxes(STATE, undefined).state, STATE)
37
37
  assert.equal(pickAxes(STATE, []).state, STATE)
38
38
  assert.equal(pickAxes(STATE, null).state, STATE)
@@ -1,6 +1,6 @@
1
1
  /*
2
- * ★ 캡스톤 · 스트림라인 전 사슬(in-process) — 모델 → board → 커널 → 라이브 payload.
3
- * master(템플릿 산출 동형) → masterToTwin → WmsKernel.loadBoard → tick → getSnapshot → buildEntityDeltas.
2
+ * ★ 캡스톤 · 스트림라인 전 사슬(in-process) — 모델 → model → 커널 → 라이브 payload.
3
+ * master(템플릿 산출 동형) → masterToTwin → WmsKernel.loadTwinModel → tick → getSnapshot → buildEntityDeltas.
4
4
  * 이 한 테스트가 조각들이 "함께" 동작함을 증명(개별 단위테스트 위의 통합).
5
5
  */
6
6
  import { test } from 'node:test'
@@ -8,7 +8,7 @@ import assert from 'node:assert/strict'
8
8
 
9
9
  import { WmsKernel, TwinRuntime } from '@operato/twin-kernel'
10
10
  import { masterToTwin } from '../server/service/reference/reference-master.ts'
11
- import { buildEntityDeltas } from '../server/engine/entity-delta.ts'
11
+ import { buildEntityDeltas } from '../shared/entity-delta.ts'
12
12
 
13
13
  const wmsMaster = () => ({
14
14
  source: 'wh-e2e',
@@ -35,14 +35,14 @@ const snapshotState = (kernel: any) => {
35
35
  }
36
36
 
37
37
  test('★ 캡스톤: master → masterToTwin → 커널 로드 → tick → 라이브 payload (전 사슬 결합)', () => {
38
- // 1) 모델 → board 토폴로지
39
- const { board } = masterToTwin(wmsMaster())
40
- assert.equal(board.locations.length, 5, '노드 5')
41
- assert.equal(board.equipment.length, 2, '무버 2')
38
+ // 1) 모델 → model 토폴로지
39
+ const { model } = masterToTwin(wmsMaster())
40
+ assert.equal(model.locations.length, 5, '노드 5')
41
+ assert.equal(model.equipment.length, 2, '무버 2')
42
42
 
43
43
  // 2) 커널 로드 + 런타임 tick
44
44
  const kernel = new WmsKernel('e2e')
45
- kernel.loadBoard(board)
45
+ kernel.loadTwinModel(model)
46
46
  const runtime = new TwinRuntime(kernel)
47
47
  for (let i = 0; i < 5; i++) runtime.tick(1000)
48
48
 
@@ -51,9 +51,9 @@ test('★ 캡스톤: master → masterToTwin → 커널 로드 → tick → 라
51
51
  const deltas = buildEntityDeltas(state, 't1')
52
52
  const tags = new Set(deltas.map(d => d.tag))
53
53
 
54
- // 4) 보드의 모든 노드·무버가 payload 에 도달(커널→보드 결합 성립)
55
- for (const n of board.locations) assert.ok(tags.has(n.id), `노드 ${n.id} payload 도달`)
56
- for (const m of board.equipment) assert.ok(tags.has(m.id), `무버 ${m.id} payload 도달`)
54
+ // 4) 모델의 모든 노드·무버가 payload 에 도달(커널→보드 결합 성립)
55
+ for (const n of model.locations) assert.ok(tags.has(n.id), `노드 ${n.id} payload 도달`)
56
+ for (const m of model.equipment) assert.ok(tags.has(m.id), `무버 ${m.id} payload 도달`)
57
57
 
58
58
  // 5) 능력별 상태필드가 실림 — storable 노드 occupancy/capacity, mobile+operable 무버 location/status
59
59
  const rack = deltas.find(d => d.tag === 'rack-1')!.data as any
@@ -63,11 +63,11 @@ test('★ 캡스톤: master → masterToTwin → 커널 로드 → tick → 라
63
63
  assert.ok('location' in fk && 'status' in fk, 'mobile+operable payload 필드')
64
64
  })
65
65
 
66
- test('캡스톤: 결정성 — 같은 board+tick 은 같은 payload 시그니처', () => {
66
+ test('캡스톤: 결정성 — 같은 model+tick 은 같은 payload 시그니처', () => {
67
67
  const run = () => {
68
- const { board } = masterToTwin(wmsMaster())
68
+ const { model } = masterToTwin(wmsMaster())
69
69
  const kernel = new WmsKernel('det')
70
- kernel.loadBoard(board)
70
+ kernel.loadTwinModel(model)
71
71
  const runtime = new TwinRuntime(kernel)
72
72
  for (let i = 0; i < 8; i++) runtime.tick(1000)
73
73
  return JSON.stringify(buildEntityDeltas(snapshotState(kernel), 't1'))
@@ -51,11 +51,11 @@ const boardWith = (bins: number, forklifts: number): any => ({
51
51
  })
52
52
 
53
53
  /** 커널을 실제로 굴려 봉투를 얻는다 — 손으로 지어낸 모양은 검증이 아니다. */
54
- function realEvents(board: any, ticks: number): any[] {
54
+ function realEvents(model: any, ticks: number): any[] {
55
55
  const journal = new EventJournal()
56
56
  const k = new WmsKernel('struct')
57
57
  k.onEvent((e: any) => journal.append(e))
58
- k.loadBoard(board)
58
+ k.loadTwinModel(model)
59
59
  k.scenario.load({
60
60
  seed: 11,
61
61
  generators: [{ kind: 'inbound-arrival', rate: { distribution: 'constant', meanPerHour: 120 }, content: { skuMix: [{ gtin: GTIN, weight: 1 }], qtyPerLine: { min: 1, max: 1 } } }]
@@ -94,10 +94,10 @@ beforeEach(async () => {
94
94
  })
95
95
 
96
96
  test('같은 구조로 다시 프로비저닝하면 리비전이 늘지 않는다 — 부팅마다 늘면 이력이 잘게 쪼개진다', async () => {
97
- const board = boardWith(2, 1)
98
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
99
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
100
- await TwinEngine.provision(domainId, INSTANCE, 'wms', JSON.parse(JSON.stringify(board)))
97
+ const model = boardWith(2, 1)
98
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
99
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
100
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', JSON.parse(JSON.stringify(model)))
101
101
 
102
102
  const revs = await ds.getRepository(TwinStructure).find({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
103
103
  assert.equal(revs.length, 1)
@@ -165,7 +165,7 @@ test('구조 번호가 없는 옛 행은 **가장 오래된 구조**로 접는
165
165
  assert.equal(state.equipment.length, 3, '마지막 구조가 자원의 진실이다')
166
166
  })
167
167
 
168
- test('리비전이 하나뿐이면 예전과 똑같이 접는다 — 갈아타기가 결과를 바꾸지 않는다', async () => {
168
+ test('리비전이 하나뿐이면 예전과 똑같이 접는다 — 구조 전환이 결과를 바꾸지 않는다', async () => {
169
169
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(3, 2))
170
170
  const events = realEvents(boardWith(3, 2), 300)
171
171
  await TwinEngine.persistBatch(domainId, INSTANCE, events, 0)
@@ -203,7 +203,7 @@ test('시간여행은 **그때의 공장**을 보여준다 — 과거에 최신
203
203
  assert.equal(past.locations.filter((n: any) => n.type === 'storage').length, 2, '그때는 자리도 둘이었다')
204
204
  })
205
205
 
206
- test('구조 이력이 **무엇이 달라졌는지** 말한다 — 보드 두 개를 눈으로 비교할 수는 없다', async () => {
206
+ test('구조 이력이 **무엇이 달라졌는지** 알린다 — 보드 두 개를 눈으로 비교할 수는 없다', async () => {
207
207
  await TwinEngine.provision(domainId, INSTANCE, 'wms', boardWith(2, 1))
208
208
  const first = realEvents(boardWith(2, 1), 200)
209
209
  await TwinEngine.persistBatch(domainId, INSTANCE, first, 0)
@@ -249,12 +249,12 @@ test('지문 표기가 낡았을 뿐이면 리비전을 만들지 않는다 —
249
249
  * 바뀐 트윈들이 재기동 한 번에 전부 "구조가 바뀌었다" 로 기록됐다. 저장된 **보드**와 비교하면
250
250
  * 그런 일이 없다.
251
251
  */
252
- const board = boardWith(2, 1)
253
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
252
+ const model = boardWith(2, 1)
253
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
254
254
  const table = ds.getMetadata(TwinStructure).tableName
255
255
  await ds.query(`UPDATE "${table}" SET signature = 'old-style-signature-from-a-previous-algorithm'`)
256
256
 
257
- await TwinEngine.provision(domainId, INSTANCE, 'wms', board)
257
+ await TwinEngine.provision(domainId, INSTANCE, 'wms', model)
258
258
 
259
259
  const revs = await ds.getRepository(TwinStructure).find({ where: { domain: { id: domainId }, instanceId: INSTANCE } })
260
260
  assert.equal(revs.length, 1, '공장은 그대로인데 리비전이 늘면 이력이 거짓으로 부푼다')
@@ -32,7 +32,7 @@ let beta: string
32
32
  /** 두 테넌트가 같은 벤더 시스템을 연결했을 때 실제로 생기는 id. */
33
33
  const SHARED_ID = 'sap-ewm-1710'
34
34
 
35
- const board = (): any => ({
35
+ const model = (): any => ({
36
36
  spaceId: 'shared-space',
37
37
  utcOffsetMinutes: 0,
38
38
  locations: [
@@ -61,8 +61,8 @@ before(async () => {
61
61
  beta = b.id
62
62
 
63
63
  /* 두 테넌트가 **같은 id** 로 각자 프로비저닝한다 — DB 유일성이 `[domain, instanceId]` 라 허용된다. */
64
- await TwinEngine.provision(alpha, SHARED_ID, 'wms', board())
65
- await TwinEngine.provision(beta, SHARED_ID, 'wms', board())
64
+ await TwinEngine.provision(alpha, SHARED_ID, 'wms', model())
65
+ await TwinEngine.provision(beta, SHARED_ID, 'wms', model())
66
66
  })
67
67
 
68
68
  /**
@@ -89,7 +89,7 @@ test('두 테넌트가 같은 id 로 프로비저닝된다 — 서로를 막지
89
89
  assert.equal(listB.filter((x: any) => x.instanceId === SHARED_ID).length, 1)
90
90
  })
91
91
 
92
- test('한쪽만 기동하면 다른 쪽은 **돌지 않는 것으로** 보인다 — 이 회귀가 핵심', async () => {
92
+ test('한쪽만 기동하면 다른 쪽은 **정지한 것으로** 보인다 — 이 회귀가 핵심', async () => {
93
93
  await TwinEngine.startFromRegistry(alpha, SHARED_ID)
94
94
 
95
95
  assert.equal(TwinEngine.owns(alpha, SHARED_ID), true, 'alpha 는 자기 트윈을 갖는다')
@@ -127,7 +127,7 @@ test('접근자가 테넌트별로 답한다 — 커널·스냅샷·런타임',
127
127
  }
128
128
  })
129
129
 
130
- test('한쪽을 정지해도 다른 쪽은 계속 돈다 — 남의 런타임을 지우지 않는다', async () => {
130
+ test('한쪽을 정지해도 다른 쪽은 계속 동작한다 — 남의 런타임을 지우지 않는다', async () => {
131
131
  await TwinEngine.startFromRegistry(alpha, SHARED_ID)
132
132
  await TwinEngine.startFromRegistry(beta, SHARED_ID)
133
133
 
@@ -0,0 +1,83 @@
1
+ /*
2
+ * 커맨드 감사 기록 — 이 검사가 지키는 것은 **기록이 증명력을 갖는가** 다.
3
+ *
4
+ * 감사 기록의 가치는 "남아 있다" 가 아니라 "믿을 수 있다" 에 있다. 클라이언트가 보낸 신원을 그대로
5
+ * 적으면 위조가 자유로워 아무것도 증명하지 못하고, 거절된 시도를 안 남기면 "무엇을 하려 했는가" 가
6
+ * 사라진다. 그 둘이 이 파일의 핵심 검사다.
7
+ */
8
+ import { test } from 'node:test'
9
+ import assert from 'node:assert/strict'
10
+
11
+ import { buildCommandAudit, normalizeOrigin } from '../server/service/twin-audit/command-audit.ts'
12
+
13
+ const AT = new Date('2026-08-06T12:00:00.000Z')
14
+
15
+ test('명령 종류를 그대로 싣는다 — 무엇을 하려 했는지가 감사의 시작이다', () => {
16
+ const row = buildCommandAudit({ command: { commandId: 'c1', type: 'order.hold' }, ack: { accepted: true }, at: AT })
17
+ assert.equal(row.commandType, 'order.hold')
18
+ assert.equal(row.commandId, 'c1')
19
+ })
20
+
21
+ test('신원은 서버가 준 것만 적는다 — 커맨드에 실려 온 신원 주장은 무시한다', () => {
22
+ const row = buildCommandAudit({
23
+ /* 위조 시도: 클라이언트가 자기를 관리자라고 주장한다. */
24
+ command: { type: 'order.hold', actorId: 'admin', actorEmail: 'admin@corp', actorName: '관리자' },
25
+ ack: { accepted: true },
26
+ actor: { id: 'u-7', email: 'kim@corp', name: '김현장' },
27
+ at: AT
28
+ })
29
+ assert.equal(row.actorId, 'u-7')
30
+ assert.equal(row.actorEmail, 'kim@corp')
31
+ assert.equal(row.actorName, '김현장')
32
+ })
33
+
34
+ test('세션에 사용자가 없으면 비운다 — 모르는 것을 지어내지 않는다', () => {
35
+ const row = buildCommandAudit({ command: { type: 'order.hold' }, ack: { accepted: true }, at: AT })
36
+ assert.equal(row.actorId, undefined)
37
+ assert.equal(row.actorEmail, undefined)
38
+ })
39
+
40
+ test('커널이 거절한 시도도 남는다 — 무엇을 하려 했는지가 감사의 절반이다', () => {
41
+ const row = buildCommandAudit({
42
+ command: { type: 'equipment.repair', args: { id: 'cutter-1' } },
43
+ ack: { accepted: false, errorCode: 'not-broken' },
44
+ at: AT
45
+ })
46
+ assert.equal(row.accepted, false)
47
+ assert.equal(row.errorCode, 'not-broken')
48
+ assert.deepEqual(row.args, { id: 'cutter-1' })
49
+ })
50
+
51
+ test('출처는 아는 값만 — 모르면 unknown 이지 사람이 한 것으로 치지 않는다', () => {
52
+ assert.equal(normalizeOrigin('ai'), 'ai')
53
+ assert.equal(normalizeOrigin('user'), 'user')
54
+ assert.equal(normalizeOrigin('automation'), 'automation')
55
+ assert.equal(normalizeOrigin(undefined), 'unknown')
56
+ assert.equal(normalizeOrigin('관리자승인'), 'unknown')
57
+ })
58
+
59
+ test('근거는 문자열일 때만 싣는다 — 객체를 그대로 넣어 화면이 [object Object] 를 띄우지 않게', () => {
60
+ const ok = buildCommandAudit({ command: { type: 't', reason: '대기 15분 절감' }, ack: {}, at: AT })
61
+ assert.equal(ok.reason, '대기 15분 절감')
62
+ const bad = buildCommandAudit({ command: { type: 't', reason: { why: 'x' } }, ack: {}, at: AT })
63
+ assert.equal(bad.reason, undefined)
64
+ })
65
+
66
+ test('args 가 지나치게 크면 자르되 잘랐다고 알린다 — 조용히 버리지 않는다', () => {
67
+ const huge = { blob: 'x'.repeat(9000) }
68
+ const row = buildCommandAudit({ command: { type: 't', args: huge }, ack: {}, at: AT })
69
+ assert.equal(row.argsTruncated, true)
70
+ assert.equal(typeof row.args, 'string')
71
+ assert.ok((row.args as string).length <= 4000)
72
+ })
73
+
74
+ test('작은 args 는 자르지 않고 원형 그대로 — 나중에 따질 수 있어야 한다', () => {
75
+ const row = buildCommandAudit({ command: { type: 't', args: { id: 'a', qty: 3 } }, ack: {}, at: AT })
76
+ assert.equal(row.argsTruncated, false)
77
+ assert.deepEqual(row.args, { id: 'a', qty: 3 })
78
+ })
79
+
80
+ test('시각은 호출부가 준 서버 시각을 그대로 쓴다 — 클라이언트 시계를 믿지 않는다', () => {
81
+ const row = buildCommandAudit({ command: { type: 't', eventTime: '1999-01-01T00:00:00.000Z' }, ack: {}, at: AT })
82
+ assert.equal(row.eventTime.toISOString(), AT.toISOString())
83
+ })