@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,226 @@
1
+ /*
2
+ * 구조 투영 — `TwinModelDef`(커널 입력 문서) → **표준 엔티티 행**(ADR-0032).
3
+ *
4
+ * ── 왜 DB 검사인가 ─────────────────────────────────────────────────────────
5
+ * 이 코드의 값어치는 **DB 가 대신 지켜 주는 것**에 있다 — 끊어진 참조를 FK 가 막고, 원본 없는 행을
6
+ * NOT NULL 이 막는다. 순수 함수 검사로는 그게 하나도 확인되지 않는다. 그래서 실제 DataSource 를
7
+ * 세운다(멀티DB 규약상 sqlite 로 도는 것이 5개 드라이버 공통의 최소선이기도 하다).
8
+ *
9
+ * ── 이 검사가 지키는 것 ────────────────────────────────────────────────────
10
+ * · 문서의 세 축이 행으로 선다
11
+ * · 설비→자리가 **진짜 FK** 로 이어진다(문자열 대조가 아니라)
12
+ * · 끊어진 참조는 **이름을 대고 보고**되고, 행 자체는 남는다(막지 않되 침묵하지 않는다)
13
+ * · **전량 교체** — 두 번 돌려도 늘지 않고, 원본에서 사라진 것은 행에서도 사라진다
14
+ * · 원본 표식(`sourceRef`·`ingestedAt`) 없는 행은 만들 수 없다(캐시라는 사실이 구조로 지켜진다)
15
+ * · 소요시간은 ISO 8601 **문자열 그대로** 남는다(분으로 접으면 초가 조용히 사라진다)
16
+ */
17
+ import { test, before, after, beforeEach } from 'node:test'
18
+ import assert from 'node:assert/strict'
19
+ import { createRequire } from 'node:module'
20
+ import type { DataSource } from 'typeorm'
21
+
22
+ const req = createRequire(import.meta.url)
23
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain } = req('@things-factory/shell')
24
+ const { entities: authEntities } = req('@things-factory/auth-base')
25
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
26
+ const { projectStructure } = req('../dist-server/service/twin-model/project-structure.js')
27
+ const { TwinInstance } = req('../dist-server/service/twin-instance/twin-instance.js')
28
+ const { TwinSpace } = req('../dist-server/service/twin-space/twin-space.js')
29
+ const { TwinArea } = req('../dist-server/service/twin-space/twin-area.js')
30
+ const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
31
+ const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
32
+ const { TwinOperation } = req('../dist-server/service/twin-model/twin-operation.js')
33
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
34
+
35
+ let ds: DataSource
36
+ let domainId: string
37
+
38
+ const INSTANCE = 'projection-probe'
39
+ const SOURCE = 'projection-probe'
40
+
41
+ /** 실제 트레일러 트윈과 같은 모양의 문서(축소판). */
42
+ const model = () => ({
43
+ locations: [
44
+ { id: 'cut-station', type: 'cut-station', name: '프레임 절단', capacity: 20, parentId: 'a-floor' },
45
+ { id: 'weld-station', type: 'weld-station', capacity: 20, parentId: 'a-floor' },
46
+ { id: 'orphan-bin', type: 'storage', capacity: 5, parentId: 'no-such-area' }
47
+ ],
48
+ equipment: [
49
+ { id: 'cutter-1', kind: 'cutter', name: '절단기 1호', homeLocation: 'cut-station', mtbfMs: 3600000,
50
+ properties: [{ id: 'speed', value: '1.2', dataType: 'xs:double', uom: 'MTS' }] },
51
+ { id: 'welder-1', kind: 'welder', homeLocation: 'weld-station' },
52
+ { id: 'ghost-1', kind: 'welder', homeLocation: 'no-such-location' }
53
+ ],
54
+ operations: [
55
+ { key: 'cut', label: 'twin.tpl.x.cut', intent: 'process', locationType: 'cut-station', resourceType: 'cutter',
56
+ duration: 'PT1H10M30S', variability: { distribution: 'triangular', min: 'PT1H', max: 'PT1H30M' },
57
+ parameters: [{ id: 'yield', value: '0.995' }],
58
+ /* 네 자원 명세 — 설비는 `resourceType`, 나머지 셋은 아래. */
59
+ personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }],
60
+ physicalAssetSpecification: [{ assetClass: 'jig', quantity: 1 }],
61
+ materialSpecification: [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }] }
62
+ ]
63
+ })
64
+
65
+ before(async () => {
66
+ ds = new TypeOrmDataSource({
67
+ type: 'sqlite',
68
+ database: ':memory:',
69
+ synchronize: true,
70
+ dropSchema: true,
71
+ namingStrategy: new NamingStrategy(),
72
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
73
+ logging: false
74
+ })
75
+ await ds.initialize()
76
+ addDataSource('default', ds)
77
+ const d = await ds.getRepository(Domain).save({ name: 'projection-test', subdomain: 'projection-test' } as any)
78
+ domainId = d.id
79
+
80
+ await ds.getRepository(TwinInstance).save({
81
+ domain: { id: domainId },
82
+ instanceId: INSTANCE,
83
+ kind: 'mes',
84
+ realityMode: 'sim-experiment',
85
+ purpose: 'operational',
86
+ status: 'stopped',
87
+ model: {}
88
+ } as any)
89
+
90
+ /* 구역은 model 밖(호스트 개념) — 자리가 FK 로 이걸 가리킨다. */
91
+ const space = await ds.getRepository(TwinSpace).save({ domain: { id: domainId }, spaceId: 'sp-1', name: '공장' } as any)
92
+ await ds.getRepository(TwinArea).save({ domain: { id: domainId }, space: { id: space.id }, areaId: 'a-floor', name: '생산 플로어' } as any)
93
+ })
94
+
95
+ after(async () => {
96
+ await ds?.destroy()
97
+ })
98
+
99
+ beforeEach(async () => {
100
+ await projectStructure(domainId, INSTANCE, model(), SOURCE)
101
+ })
102
+
103
+ test('문서의 세 축이 행으로 선다', async () => {
104
+ assert.equal(await ds.getRepository(TwinLocation).count(), 3)
105
+ assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
106
+ assert.equal(await ds.getRepository(TwinOperation).count(), 1)
107
+ })
108
+
109
+ test('설비→자리가 진짜 FK 로 이어진다 — 문자열 대조가 아니다', async () => {
110
+ const eq = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'cutter-1' }, relations: ['homeLocation'] })
111
+ assert.equal(eq?.homeLocation?.locationId, 'cut-station', 'FK 로 자리를 따라갈 수 있어야 한다')
112
+ /* 자리 쪽에서도 같은 행이다 — 문자열이었으면 같은 값이지만 같은 행은 아니다. */
113
+ const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' } })
114
+ assert.equal(eq?.homeLocation?.id, loc?.id)
115
+ })
116
+
117
+ test('자리→구역이 FK 로 이어진다 — 커널이 "밖" 이라 부른 참조가 호스트에서는 평범한 FK 다', async () => {
118
+ const loc = await ds.getRepository(TwinLocation).findOne({ where: { locationId: 'cut-station' }, relations: ['area'] })
119
+ assert.equal(loc?.area?.areaId, 'a-floor')
120
+ })
121
+
122
+ test('끊어진 참조는 이름을 대고 보고된다 — 막지 않되 침묵하지 않는다', async () => {
123
+ const report = await projectStructure(domainId, INSTANCE, model(), SOURCE)
124
+ assert.deepEqual(report.unresolvedHome, ['ghost-1→no-such-location'])
125
+ assert.deepEqual(report.unresolvedArea, ['orphan-bin→no-such-area'])
126
+
127
+ /* 행 자체는 남는다 — 참조 하나 때문에 설비가 통째로 사라지면 그게 더 나쁜 거짓말이다. */
128
+ const ghost = await ds.getRepository(TwinEquipment).findOne({ where: { equipmentId: 'ghost-1' }, relations: ['homeLocation'] })
129
+ assert.ok(ghost, '끊어진 참조를 가진 설비도 행으로 남는다')
130
+ assert.equal(ghost?.homeLocation, null, '이을 수 없는 참조는 비운다(엉뚱한 자리에 붙이지 않는다)')
131
+ })
132
+
133
+ test('전량 교체 — 두 번 돌려도 늘지 않는다', async () => {
134
+ await projectStructure(domainId, INSTANCE, model(), SOURCE)
135
+ await projectStructure(domainId, INSTANCE, model(), SOURCE)
136
+ assert.equal(await ds.getRepository(TwinLocation).count(), 3)
137
+ assert.equal(await ds.getRepository(TwinEquipment).count(), 3)
138
+ })
139
+
140
+ test('원본에서 사라진 것은 행에서도 사라진다 — 캐시는 원본을 따라간다', async () => {
141
+ const shrunk = model()
142
+ shrunk.locations = shrunk.locations.filter(l => l.id !== 'weld-station')
143
+ shrunk.equipment = shrunk.equipment.filter(m => m.id !== 'welder-1')
144
+ await projectStructure(domainId, INSTANCE, shrunk, SOURCE)
145
+ assert.equal(await ds.getRepository(TwinLocation).count(), 2)
146
+ assert.equal(await ds.getRepository(TwinEquipment).count(), 2)
147
+ assert.equal(await ds.getRepository(TwinLocation).countBy({ locationId: 'weld-station' } as any), 0)
148
+ })
149
+
150
+ test('원본 표식 없는 행은 만들 수 없다 — 캐시라는 사실이 구조로 지켜진다', async () => {
151
+ await assert.rejects(
152
+ () =>
153
+ ds.getRepository(TwinLocation).save({
154
+ domain: { id: domainId },
155
+ locationId: 'no-origin',
156
+ type: 'storage'
157
+ /* sourceRef · ingestedAt 없음 */
158
+ } as any),
159
+ /NOT NULL|constraint/i,
160
+ '출처 없는 행이 들어가면 "원본에서 온 캐시" 라는 규율이 구조로는 안 지켜지는 것이다'
161
+ )
162
+ })
163
+
164
+ test('소요시간은 ISO 8601 문자열 그대로 — 분으로 접으면 초가 조용히 사라진다', async () => {
165
+ const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
166
+ assert.equal(op?.duration, 'PT1H10M30S')
167
+ /* 변동성·파라미터는 도메인마다 형이 달라 원형 그대로 싣는다. */
168
+ assert.equal(op?.variability?.distribution, 'triangular')
169
+ assert.deepEqual(op?.parameters, [{ id: 'yield', value: '0.995' }])
170
+ })
171
+
172
+ test('등록되지 않은 트윈에는 투영하지 않는다 — 주인 없는 행을 만들지 않는다', async () => {
173
+ await assert.rejects(() => projectStructure(domainId, 'not-registered', model(), SOURCE), /not registered/)
174
+ })
175
+
176
+ test('네 자원 명세가 함께 옮겨진다 — 선언한 것만 제약이 되므로 빠지면 화면이 거짓이 된다', async () => {
177
+ /*
178
+ * ISA-95 는 공정이 무엇을 필요로 하는지를 넷으로 말한다: 설비·인원·자산·자재. 셋이 투영에서
179
+ * 떨어져 있어서, 조회 화면이 "원본이 말한 값" 이라 적고도 넷 중 하나만 보였다 — 사람이 필요한
180
+ * 공정을 보고 **사람이 필요 없다고 결론 내게** 되어 있었다.
181
+ */
182
+ const op = await ds.getRepository(TwinOperation).findOne({ where: { operationKey: 'cut' } })
183
+ assert.deepEqual(op?.personnelSpecification, [{ personnelClass: 'cutter-op', quantity: 1 }], '인원 명세가 행에 남아야 한다')
184
+ assert.deepEqual(op?.physicalAssetSpecification, [{ assetClass: 'jig', quantity: 1 }], '자산 명세도 남아야 한다')
185
+ assert.deepEqual(op?.materialSpecification, [{ materialDefinition: 'steel', use: 'consumed', quantity: 2 }], '자재 명세도 남아야 한다')
186
+ })
187
+
188
+ test('이름이 행까지 온다 — 이름이 없으면 화면에 id 만 남는다', async () => {
189
+ /*
190
+ * 투영이 마스터의 `name` 을 버리고 있었다. 행에 이름이 없으면 어떤 화면도 이름을 만들 수 없어서
191
+ * 목록·표·관계 그림이 모두 `cut-station` 을 찍었다 — 표면마다 따로 보이지만 결함은 여기 하나다.
192
+ */
193
+ const loc = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'cut-station' } as any })
194
+ assert.equal(loc!.name, '프레임 절단', '자리의 이름이 행에 실려야 한다')
195
+
196
+ const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
197
+ assert.equal(eq!.name, '절단기 1호', '설비의 이름이 행에 실려야 한다')
198
+
199
+ /* 공정은 마스터가 `label` 로 말한다 — 행에서는 규약의 이름으로 둔다(어휘를 하나로). */
200
+ const op = await ds.getRepository(TwinOperation).findOne({ where: { domain: { id: domainId } as any, operationKey: 'cut' } as any })
201
+ assert.equal(op!.name, 'twin.tpl.x.cut', '공정의 이름이 행에 실려야 한다')
202
+
203
+ /* **이름 없는 원본은 이름 없이 남는다** — id 를 이름으로 베끼면 "이름이 있다" 는 거짓이 되고,
204
+ 화면은 이름 없음을 알아볼 수 없게 된다. */
205
+ const noname = await ds.getRepository(TwinLocation).findOne({ where: { domain: { id: domainId } as any, locationId: 'weld-station' } as any })
206
+ assert.equal(noname!.name ?? null, null, '원본이 이름을 말하지 않았으면 비어 있어야 한다')
207
+ })
208
+
209
+ test('자원 속성이 행까지 온다 — 소비처가 실제로 읽는 사실이다(주행 속도)', async () => {
210
+ /*
211
+ * 모델에는 있는데 행에 자리가 없어 투영이 흘리고 있었다. 그러면 조회 화면은 "원본이 말한 값" 이라
212
+ * 적고도 속성을 못 보인다 — 이름이 같은 식으로 사라졌던 것과 같은 부류다.
213
+ *
214
+ * 단위를 값과 함께 담는지도 본다: 단위 없는 "1.2" 는 초속인지 시속인지 말하지 않는다.
215
+ */
216
+ const eq = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'cutter-1' } as any })
217
+ const props = (eq!.properties ?? []) as any[]
218
+ assert.equal(props.length, 1, '선언한 속성이 행에 실려야 한다')
219
+ assert.equal(props[0].id, 'speed')
220
+ assert.equal(props[0].value, '1.2')
221
+ assert.equal(props[0].uom, 'MTS', '단위가 값과 함께 남아야 한다')
222
+
223
+ /* 선언하지 않은 자원은 **비어 있다** — 빈 배열로 "선언했다" 고 말하지 않는다. */
224
+ const noneProps = await ds.getRepository(TwinEquipment).findOne({ where: { domain: { id: domainId } as any, equipmentId: 'welder-1' } as any })
225
+ assert.equal(noneProps!.properties ?? null, null)
226
+ })
@@ -0,0 +1,272 @@
1
+ /*
2
+ * 선언 적합성(투영 편) — **모델이 말한 사실은 행에 남고, 행에 남은 것은 화면까지 간다.**
3
+ *
4
+ * ── 왜 필요한가 ─────────────────────────────────────────────────────────────
5
+ * 같은 이름의 하네스가 두 벌 있다. 커널 편은 *선언 ⊆ 상태* 를 지키고(kernel/declaration-in-state),
6
+ * 경계 편은 *원본 → 모델* 을 지킨다(declaration-reaches-model). 그런데 사실이 새던 구간은 **셋**이었고,
7
+ * 남은 하나가 이것이다: **모델 → DB 행 → 조회 응답.**
8
+ *
9
+ * 여기서 잃은 것들: 자리·설비의 이름(화면이 `cut-station` 을 찍었다) · GS1 참조 · 자원 속성 ·
10
+ * 시험 명세 참조 · 시험 결과. 전부 **투영 함수의 한 줄이 빠진** 것이었고, 그때는 사람이 하나씩 찾았다.
11
+ *
12
+ * 타입 검사는 못 잡는다: 컬럼이 거의 다 `nullable` 이라 **안 채우는 것이 합법**이다.
13
+ *
14
+ * ── 무엇이 기준인가 ─────────────────────────────────────────────────────────
15
+ * **엔티티의 컬럼**이다(TypeORM 메타데이터에서 실행 중에 읽는다 — 손으로 적지 않는다). 컬럼이 있다는
16
+ * 것은 "이 사실이 여기 앉는다" 는 선언이고, 앉을 자리를 만들어 두고 안 채우면 그 컬럼은 영원히 빈다.
17
+ *
18
+ * 그래서 컬럼마다 셋을 요구한다:
19
+ * ① 픽스처가 모델에 그 값을 싣는다(값을 줄 방법을 모르면 **검사가 실패한다** — 조용히 건너뛰면
20
+ * 그 컬럼은 아무도 안 보는 컬럼이 된다)
21
+ * ② 투영을 지나 행에 남는다
22
+ * ③ 조회 응답에 나온다(화면이 읽는 자리)
23
+ *
24
+ * 기반 컬럼(id·관계·감사·출처)은 모델이 말하는 사실이 아니므로 이유와 함께 뺀다.
25
+ */
26
+ import { test, before, after } from 'node:test'
27
+ import assert from 'node:assert/strict'
28
+ import { createRequire } from 'node:module'
29
+ import type { DataSource } from 'typeorm'
30
+
31
+ const req = createRequire(import.meta.url)
32
+ const { NamingStrategy, addDataSource, entities: shellEntities, Domain, getRepository } = req('@things-factory/shell')
33
+ const { entities: authEntities } = req('@things-factory/auth-base')
34
+ const { entities: twinEntities } = req('../dist-server/service/index.js')
35
+ const { TwinEngine } = req('../dist-server/engine/index.js')
36
+ const { TwinModelItemQuery } = req('../dist-server/service/twin-model/twin-model-item-query.js')
37
+ const { TwinLocation } = req('../dist-server/service/twin-model/twin-location.js')
38
+ const { TwinEquipment } = req('../dist-server/service/twin-model/twin-equipment.js')
39
+ const { TwinOperation } = req('../dist-server/service/twin-model/twin-operation.js')
40
+ const { DataSource: TypeOrmDataSource } = req('typeorm')
41
+
42
+ let ds: DataSource
43
+ let domainId: string
44
+ let q: any
45
+ let ctx: any
46
+
47
+ const INSTANCE = 'projection-probe'
48
+
49
+ /**
50
+ * 모델이 말하는 사실이 **아닌** 컬럼 — 이유와 함께.
51
+ *
52
+ * 비어 있으면 안 된다: 여기 적지 않으면 검사가 그 컬럼에 값을 요구하고, 그것은 틀린 요구다.
53
+ * 반대로 여기에 슬쩍 올리면 감시가 사라지므로 **이유를 함께 적는다**(리뷰가 볼 자리다).
54
+ */
55
+ const NOT_FROM_MODEL: Record<string, string> = {
56
+ id: '행의 열쇠 — 모델이 정하지 않는다(DB 가 발급한다)',
57
+ locationId: '개체 식별자 — 대조의 열쇠라 값 비교에서 뺀다(존재는 조회로 이미 확인한다)',
58
+ equipmentId: '같음',
59
+ operationKey: '같음',
60
+ sourceRef: '어디서 왔나 — 투영이 남기는 근거다(모델의 사실이 아니라 옮긴 행위의 기록)',
61
+ ingestedAt: '언제 옮겼나 — 같은 이유',
62
+ seq: '문서의 차례 — 투영이 넣는 내부 컬럼이다(원본이 말한 값이 아니다)'
63
+ }
64
+
65
+ /** 컬럼별 값 — 이름으로 준다. 여기 없는 컬럼이 검사 대상에 들면 하네스가 실패한다. */
66
+ const VALUE: Record<string, unknown> = {
67
+ name: '절단 스테이션',
68
+ description: '강판을 자르는 자리',
69
+ type: 'cut-station',
70
+ kind: 'cutter',
71
+ capacity: 40,
72
+ parallelism: 2,
73
+ level: 'WorkCenter',
74
+ gs1Id: 'urn:epc:id:sgln:0614141.00001.0',
75
+ homeLocation: 'cut-station',
76
+ mtbfMs: 600000,
77
+ mttrMs: 60000,
78
+ properties: [{ id: 'speed', value: '1.2', dataType: 'xs:double', uom: 'MTS' }],
79
+ testSpecificationIds: ['cut-cert'],
80
+ testResults: [{ specId: 'cut-cert', result: 'pass', expiresAt: '2099-01-01T00:00:00.000Z' }],
81
+ intent: 'process',
82
+ locationType: 'cut-station',
83
+ resourceType: 'cutter',
84
+ duration: 'PT25M',
85
+ variability: { distribution: 'triangular', min: 'PT20M', max: 'PT35M', mode: 'PT24M' },
86
+ parameters: [{ id: 'yield', value: '0.98' }],
87
+ personnelSpecification: [{ personnelClass: 'cutter-op', quantity: 1 }],
88
+ equipmentSpecification: [{ equipmentClass: 'cutter', quantity: 1 }],
89
+ physicalAssetSpecification: [{ assetClass: 'euro-pallet', quantity: 1 }],
90
+ materialSpecification: [{ material: 'steel', quantity: 1 }],
91
+ label: '프레임 절단'
92
+ }
93
+
94
+ interface Case {
95
+ axis: 'locations' | 'equipment' | 'operations'
96
+ entity: any
97
+ /** 행에서 그 개체를 찾는 컬럼 — 조회가 쓰는 것과 같은 이름이어야 한다(§ROW_BACKED). */
98
+ idColumn: string
99
+ id: string
100
+ }
101
+
102
+ const CASES: Case[] = [
103
+ { axis: 'locations', entity: TwinLocation, idColumn: 'locationId', id: 'cut-station' },
104
+ { axis: 'equipment', entity: TwinEquipment, idColumn: 'equipmentId', id: 'cutter-1' },
105
+ { axis: 'operations', entity: TwinOperation, idColumn: 'operationKey', id: 'cut' }
106
+ ]
107
+
108
+ /**
109
+ * 그 엔티티에서 **모델이 채워야 하는 컬럼** — TypeORM 메타데이터에서 읽는다(손으로 적지 않는다).
110
+ * 관계·관계 id·감사 컬럼은 기반이므로 뺀다(그 판정은 조회가 쓰는 규칙과 같은 근거를 쓴다).
111
+ */
112
+ function watchedColumns(entity: any): string[] {
113
+ const meta = getRepository(entity).metadata
114
+ const infra = new Set<string>(['createdAt', 'updatedAt', 'creator', 'updater', 'creatorId', 'updaterId'])
115
+ for (const rel of meta.relations) {
116
+ infra.add(rel.propertyName)
117
+ for (const jc of rel.joinColumns) infra.add(jc.propertyName)
118
+ }
119
+ for (const rid of meta.relationIds) infra.add(rid.propertyName)
120
+ return meta.columns
121
+ .map((c: any) => c.propertyName as string)
122
+ .filter((n: string) => !infra.has(n) && !(n in NOT_FROM_MODEL))
123
+ .sort()
124
+ }
125
+
126
+ /** 모든 감시 컬럼에 값을 실은 마스터. */
127
+ function master(): any {
128
+ const fill = (c: Case): Record<string, unknown> => {
129
+ const o: Record<string, unknown> = {}
130
+ for (const col of watchedColumns(c.entity)) {
131
+ assert.ok(
132
+ col in VALUE,
133
+ `픽스처가 "${c.axis}.${col}" 에 실을 값을 모른다 — VALUE 에 하나 적어라(모델의 사실이 아니면 NOT_FROM_MODEL 에 이유와 함께).\n` +
134
+ '조용히 건너뛰면 그 컬럼은 아무도 안 보는 컬럼이 되고, 그것이 이 하네스가 막으려는 실패다.'
135
+ )
136
+ o[col] = VALUE[col]
137
+ }
138
+ return o
139
+ }
140
+ const loc = fill(CASES[0])
141
+ const eq = fill(CASES[1])
142
+ const op = fill(CASES[2])
143
+ return {
144
+ source: INSTANCE,
145
+ system: 'mes',
146
+ siteName: '투영 공장',
147
+ space: { spaceId: 'projection-site', width: 60, depth: 40, unit: 'm' },
148
+ locations: [
149
+ { id: CASES[0].id, role: 'location', ...loc },
150
+ { id: 'fg-store', type: 'fg-store', role: 'location', name: '완제품', capacity: 100 }
151
+ ],
152
+ /* 경계에서 이름이 바뀌는 것은 하나다(`homeLocationId → homeLocation`) — 경계 편 하네스가 지킨다. */
153
+ equipment: [{ id: CASES[1].id, ...eq, homeLocationId: CASES[0].id }],
154
+ testSpecifications: [{ id: 'cut-cert', name: '절단 자격' }],
155
+ personnelClasses: [{ id: 'cutter-op', description: 'twin.tpl.test.class.cutter-op' }],
156
+ assetClasses: [{ id: 'euro-pallet' }],
157
+ operations: [{ key: CASES[2].id, ...op }],
158
+ productionSpec: {
159
+ companyPrefix: '0614141',
160
+ binding: { steel: '077101' },
161
+ definition: {
162
+ materials: [{ id: 'steel' }],
163
+ routes: [{ key: 'r', steps: [CASES[2].id] }],
164
+ recipes: [{ key: 'rc', label: '프레임', route: 'r', inputs: [{ material: 'steel', qty: 1 }], outputs: [{ material: 'steel', qty: 1 }] }]
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ before(async () => {
171
+ ds = new TypeOrmDataSource({
172
+ type: 'sqlite',
173
+ database: ':memory:',
174
+ synchronize: true,
175
+ dropSchema: true,
176
+ namingStrategy: new NamingStrategy(),
177
+ entities: [...shellEntities, ...authEntities, ...twinEntities],
178
+ logging: false
179
+ })
180
+ await ds.initialize()
181
+ addDataSource('default', ds)
182
+ const d = await ds.getRepository(Domain).save({ name: 'projection-test', subdomain: 'projection-test' } as any)
183
+ domainId = d.id
184
+ q = new TwinModelItemQuery()
185
+ ctx = { state: { domain: { id: domainId } } }
186
+
187
+ await TwinEngine.ingestMaster(domainId, master())
188
+ })
189
+
190
+ after(async () => {
191
+ await TwinEngine.stop(domainId, INSTANCE).catch(() => {})
192
+ await ds?.destroy()
193
+ })
194
+
195
+ test('감시할 컬럼이 비어 있지 않다 — 비면 이 하네스는 아무것도 지키지 않는다', () => {
196
+ for (const c of CASES) {
197
+ const cols = watchedColumns(c.entity)
198
+ assert.ok(cols.length > 2, `${c.axis}: 감시 컬럼을 못 읽었다(${cols.length}개) — 메타데이터 읽기가 낡았다`)
199
+ }
200
+ })
201
+
202
+ for (const c of CASES) {
203
+ test(`${c.axis}: 모델이 말한 사실이 행에 남는다 — 투영이 버리지 않는다`, async () => {
204
+ const row: any = await getRepository(c.entity).findOne({
205
+ where: { domain: { id: domainId } as any, [c.idColumn]: c.id } as any
206
+ })
207
+ assert.ok(row, `${c.axis} "${c.id}" 행이 없다 — 사실 하나가 아니라 개체가 사라졌다`)
208
+
209
+ const lost = watchedColumns(c.entity).filter(col => row[col] === null || row[col] === undefined)
210
+ assert.deepEqual(
211
+ lost,
212
+ [],
213
+ `${c.axis}: 모델은 말했는데 행에 없는 사실 [${lost}] — 투영이 버렸다.\n` +
214
+ '컬럼이 있다는 것은 "이 사실이 여기 앉는다" 는 선언이다 — 앉을 자리를 두고 안 채우면 영원히 빈다.'
215
+ )
216
+ })
217
+
218
+ test(`${c.axis}: 행에 남은 사실이 화면까지 간다 — 조회가 감추지 않는다`, async () => {
219
+ const it = await q.twinModelItem(INSTANCE, c.axis, c.id, ctx)
220
+ assert.ok(it?.declared, `${c.axis} "${c.id}" 항목이 열리지 않았다`)
221
+ assert.equal(it.provenance?.basis, 'rows', '행에서 답해야 이 검사가 그 구간을 지킨다')
222
+
223
+ const hidden = watchedColumns(c.entity).filter(col => it.declared[col] === undefined)
224
+ assert.deepEqual(
225
+ hidden,
226
+ [],
227
+ `${c.axis}: 행에는 있는데 조회 응답에 없는 사실 [${hidden}] — 화면은 그것을 물을 방법이 없다.`
228
+ )
229
+ })
230
+ }
231
+
232
+ /*
233
+ * ★ 어떤 속성이 **효과가 있나** — 열린 것과 닫힌 것을 화면이 구별할 수 있게.
234
+ *
235
+ * 속성은 열려 있다(아무 사실이나 실어 보낼 수 있다). 그런데 값이 계산을 바꾸려면 그 규칙을 코드가
236
+ * 알아야 하고, 그것은 닫혀 있다. 그 구분이 화면에 없으면 사용자는 속성을 넣고 나서 **아무 일도 안
237
+ * 일어나는 이유**를 알 수 없다. 그래서 서버가 효과 등록부를 함께 낸다.
238
+ *
239
+ * 이 검사가 지키는 규율: **읽는 코드가 있는 것만 등록한다.** 지어낸 효과를 올리면 화면이 거짓을 말한다.
240
+ */
241
+ test('설비 항목이 효과 등록부를 함께 낸다 — 속도는 효과가 있고 나머지는 표시뿐이다', async () => {
242
+ const it = await q.twinModelItem(INSTANCE, 'equipment', 'cutter-1', ctx)
243
+ assert.ok(it?.propertyEffects, '등록부가 없으면 화면은 효과 있는 속성을 구별할 수 없다')
244
+ assert.equal(it.propertyEffects.speed, 'twin.propeffect.speed', '속도는 주행 시간 계산이 읽는다')
245
+ assert.equal('maxLoadKg' in it.propertyEffects, false, '읽는 코드가 없는 속성을 등록하면 화면이 거짓을 말한다')
246
+ })
247
+
248
+ test('등록부의 모든 항목이 읽는 자리를 밝힌다 — 지어낸 효과를 막는다', () => {
249
+ const { PROPERTY_EFFECTS } = req('../dist-server/engine/property-effects.js')
250
+ assert.ok(PROPERTY_EFFECTS.length > 0, '등록부가 비면 아무것도 지키지 않는다')
251
+ for (const e of PROPERTY_EFFECTS) {
252
+ assert.ok(e.id && e.axes?.length, `${e.id}: 어느 축의 속성인지 밝혀야 한다`)
253
+ assert.match(e.effect, /^twin\.propeffect\./, `${e.id}: 효과 문구는 i18n 키다(서버는 사람 말을 만들지 않는다)`)
254
+ assert.ok(e.readBy && e.readBy.length > 10, `${e.id}: 실제로 읽는 자리를 적어야 한다 — 없으면 지어낸 효과다`)
255
+ }
256
+ })
257
+
258
+ test('등록되지 않은 속성은 등록부에 없다 — 화면이 "표시만 됩니다" 를 말할 근거가 된다', () => {
259
+ const { propertyEffectsOf } = req('../dist-server/engine/property-effects.js')
260
+ const eq = propertyEffectsOf('equipment')
261
+ /*
262
+ * 지금 트레일러 템플릿에는 효과 없는 속성이 선언돼 있지 않다(속도 하나뿐이다). 그래서 화면에서
263
+ * "표시만 됩니다" 를 눈으로 볼 수 없다 — 그 갈래를 여기서 지킨다: 등록부에 없으면 화면이 그렇게 말한다.
264
+ */
265
+ assert.equal(eq.speed, 'twin.propeffect.speed')
266
+ for (const unknown of ['maxLoadKg', 'skill', 'nominalSpeed']) {
267
+ assert.equal(unknown in eq, false, `읽는 코드가 없는 "${unknown}" 이 등록되면 화면이 거짓을 말한다`)
268
+ }
269
+ /* 축이 다르면 효과도 다르다 — 품목 정의의 환산 계수를 설비에 말하지 않는다. */
270
+ assert.equal('perBaseUnit' in eq, false)
271
+ assert.equal(propertyEffectsOf('materialDefinitions').perBaseUnit, 'twin.propeffect.perBaseUnit')
272
+ })
@@ -0,0 +1,96 @@
1
+ /*
2
+ * 속성 효과 등록부 — **지어낸 효과를 등록하지 않고, 있는 효과를 숨기지 않는다.**
3
+ *
4
+ * ── 왜 이 시험이 있나 (2026-08-18) ──────────────────────────────────────────
5
+ * 화면은 속성마다 「무엇을 바꾸나」를 말한다. 등록되지 않은 속성에는 「표시만 됩니다」라고 한다 — 그것이
6
+ * 정직한 기본값이다. 그런데 에너지 값들(계약전력·정격·요금)은 **오래전부터 읽히고 있었는데** 등록부에
7
+ * 없었다: 사용자는 계약전력을 넣고도 「이 값은 아무것도 바꾸지 않는다」는 문장을 읽었다. 조용한 거짓말이다.
8
+ *
9
+ * 반대 방향의 위험도 있다: 효과를 등록해 두고 **읽는 코드가 없으면** 화면이 「쓰입니다」라고 말하는데
10
+ * 아무 일도 일어나지 않는다. 그래서 등록부의 각 줄이 가리키는 자리를 이 시험이 확인한다.
11
+ */
12
+ import { test } from 'node:test'
13
+ import assert from 'node:assert/strict'
14
+ import { readFileSync } from 'node:fs'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { createRequire } from 'node:module'
17
+
18
+ import { PROPERTY_EFFECTS, propertyEffectsOf, declarationTargetsOf } from '../server/engine/property-effects.ts'
19
+
20
+ const req = createRequire(import.meta.url)
21
+ /*
22
+ * 커널은 **한 벌로 묶인 산출물**을 내보낸다(`dist-cjs/index.cjs`) — 서브패스는 exports 로 막혀 있다.
23
+ * 그래서 모듈마다 읽지 않고 그 묶음 하나를 읽는다(안에 커널의 모든 읽기 코드가 들어 있다).
24
+ */
25
+ const kernelBundle = readFileSync(req.resolve('@operato/twin-kernel'), 'utf-8')
26
+ const hostSrc = (file: string): string => readFileSync(fileURLToPath(new URL(`../server/${file}`, import.meta.url)), 'utf-8')
27
+
28
+ test('등록부의 모든 줄이 자기 자리를 가리킨다 — 빈 항목이 없다', () => {
29
+ for (const e of PROPERTY_EFFECTS) {
30
+ assert.ok(e.id, '속성 id 가 있어야 한다')
31
+ assert.ok(e.axes.length, `${e.id}: 어느 축의 속성인지 말해야 한다`)
32
+ assert.match(e.effect, /^twin\.propeffect\./, `${e.id}: 효과 문장은 i18n 키다(서버는 사람 말을 만들지 않는다)`)
33
+ assert.ok(e.readBy && e.readBy.length > 10, `${e.id}: 실제로 읽는 자리를 적어야 한다`)
34
+ }
35
+ })
36
+
37
+ test('등록한 속성 id 를 읽는 코드가 실제로 있다 — 「쓰입니다」가 거짓이 되지 않게', () => {
38
+ /* 읽는 쪽 소스 — 커널(에너지·환산·주행)과 호스트(요금·주행 추정). */
39
+ const corpus = [kernelBundle, hostSrc('engine/kpi-query.ts'), hostSrc('engine/travel-estimator.ts')].join('\n')
40
+
41
+ const missing: string[] = []
42
+ for (const e of PROPERTY_EFFECTS) {
43
+ /* id 그대로 나오거나(문자열 리터럴), 커널 어휘 상수의 마지막 마디로 나온다(EMS_PROPERTY.contractKW). */
44
+ const tail = e.id.includes('.') ? e.id.split('.').pop()! : e.id
45
+ if (!corpus.includes(`'${e.id}'`) && !corpus.includes(`"${e.id}"`) && !new RegExp(`\\b${tail}\\b`).test(corpus)) missing.push(e.id)
46
+ }
47
+ assert.deepEqual(missing, [], `읽는 코드를 찾지 못한 등록 항목:\n${missing.join('\n')}`)
48
+ })
49
+
50
+ test('에너지 값들이 등록돼 있다 — 예전에는 「표시만 됩니다」라고 말했다', () => {
51
+ const ids = PROPERTY_EFFECTS.map(e => e.id)
52
+ for (const id of ['contract.kW', 'power.ratedKW', 'storage.capacityKWh', 'tariff.demandChargePerKW', 'tariff.currency'])
53
+ assert.ok(ids.includes(id), `${id} 가 등록부에 없다`)
54
+ })
55
+
56
+ test('축으로 갈라 낸다 — 그 축의 항목에만 효과를 말한다', () => {
57
+ const locations = propertyEffectsOf('locations')
58
+ const equipment = propertyEffectsOf('equipment')
59
+ assert.ok(locations['contract.kW'], '계약전력은 자리의 속성이다')
60
+ assert.equal(locations['power.ratedKW'], undefined, '설비 속성을 자리에 말하지 않는다')
61
+ assert.ok(equipment['speed'], '속도는 설비의 속성이다')
62
+ assert.equal(equipment['tariff.currency'], undefined, '요금은 자리(수전)의 속성이다')
63
+ })
64
+
65
+ test('요금은 커널이 아니라 호스트가 읽는다 — 그 사실을 readBy 가 말한다', () => {
66
+ const tariff = PROPERTY_EFFECTS.filter(e => e.id.startsWith('tariff.'))
67
+ assert.equal(tariff.length, 3)
68
+ for (const e of tariff) assert.match(e.readBy, /kpi-query/, '금액은 저널을 접는 쪽이 만든다')
69
+ })
70
+
71
+ /*
72
+ * ── 어느 대상에 얹히나 (2026-08-18) ────────────────────────────────────────
73
+ * 지게차에 계약전력을 선언할 수 있었다 — 문은 받고 화면은 「넣었습니다」라고 말하는데, 읽는 코드는
74
+ * 자리(수전)만 본다. 그 제한은 이미 등록부의 축이 알고 있으므로, 검사에 그 지식을 쓴다(두 곳에 적지
75
+ * 않는다). 등록부에 없는 속성은 **제한을 모르는 것**이므로 받아 준다 — 속성은 열려 있고 효과만 닫혀 있다.
76
+ */
77
+ test('축이 곧 얹을 수 있는 대상이다 — 목록을 따로 적지 않는다', () => {
78
+ assert.deepEqual(declarationTargetsOf('contract.kW'), ['location'])
79
+ assert.deepEqual(declarationTargetsOf('speed'), ['equipment'])
80
+ assert.deepEqual(declarationTargetsOf('operation.duration'), ['operation'])
81
+ /* 선언 대상이 아닌 축(자재 정의)은 제한을 말하지 않는다 — 이 문의 대상이 아니다. */
82
+ assert.equal(declarationTargetsOf('perBaseUnit'), undefined)
83
+ assert.equal(declarationTargetsOf('made.up.property'), undefined, '모르는 속성은 제한도 모른다')
84
+ })
85
+
86
+ test('공정 시간이 등록돼 있고, 그 값을 실어 보내는 자리를 가리킨다', () => {
87
+ const e = PROPERTY_EFFECTS.find(x => x.id === 'operation.duration')!
88
+ assert.ok(e, '공정 시간이 등록부에 있어야 한다')
89
+ assert.deepEqual(e.axes, ['operations'])
90
+ assert.match(e.readBy, /declareDurations/, '커널의 어느 창구로 가는지 말한다')
91
+ /* 실어 보내는 코드가 실제로 있다 — 등록만 해 두고 아무도 나르지 않으면 화면이 거짓을 말한다. */
92
+ const layer = hostSrc('engine/local-declarations.ts')
93
+ assert.match(layer, /OPERATION_PROPERTY\.duration/, '선언 겹이 그 속성을 안다')
94
+ assert.match(layer, /isoDurationOf/, '커널이 읽는 표기로 바꾼다')
95
+ assert.match(hostSrc('engine/twin-engine.ts'), /declareDurations\(declared\)/, '기동·구조전환 때 커널에 싣는다')
96
+ })