@things-factory/headless-twin 10.0.9 → 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 (353) 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 +125 -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 +46 -14
  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 +112 -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 +52 -15
  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 +150 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +140 -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 +150 -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/command-routing.test.ts +1 -1
  286. package/test/declaration-reaches-model.test.ts +178 -0
  287. package/test/discovery-result.test.ts +1 -1
  288. package/test/energy-topology.test.ts +113 -0
  289. package/test/entity-delta.test.ts +171 -1
  290. package/test/event-time-column.test.ts +67 -0
  291. package/test/forecast-metrics.test.ts +71 -0
  292. package/test/forecast-tuning.test.ts +42 -0
  293. package/test/gap-analytics.test.ts +34 -7
  294. package/test/ingest-bench.test.ts +6 -6
  295. package/test/ingest-running-guard.test.ts +136 -0
  296. package/test/ingest-space.test.ts +29 -1
  297. package/test/instance-cache-lifecycle.test.ts +110 -0
  298. package/test/kernel-kind-guard.test.ts +96 -0
  299. package/test/kpi-baseline-db.test.ts +3 -1
  300. package/test/kpi-baseline.test.ts +3 -3
  301. package/test/kpi-fold.test.ts +393 -2
  302. package/test/kpi-query-bench.test.ts +5 -3
  303. package/test/lineage-survives-restart.test.ts +203 -0
  304. package/test/live-feed-registry.test.ts +67 -0
  305. package/test/live-kernel-facts.test.ts +161 -0
  306. package/test/live-mirror-parity.test.ts +22 -15
  307. package/test/load-meter.test.ts +314 -0
  308. package/test/local-declarations.test.ts +554 -0
  309. package/test/master-to-twin.test.ts +121 -33
  310. package/test/model-basis.test.ts +56 -1
  311. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  312. package/test/move-space.test.ts +149 -0
  313. package/test/mutation-gate.test.ts +19 -5
  314. package/test/oee-accumulator.test.ts +26 -7
  315. package/test/operation-basis.test.ts +100 -0
  316. package/test/operations-capability-db.test.ts +165 -0
  317. package/test/project-structure-db.test.ts +226 -0
  318. package/test/projection-reaches-screen.test.ts +272 -0
  319. package/test/property-effects.test.ts +96 -0
  320. package/test/resync-origin-site.test.ts +77 -0
  321. package/test/scale-twin-bench.test.ts +5 -5
  322. package/test/snapshot-freshness.test.ts +60 -0
  323. package/test/space-integrity.test.ts +223 -0
  324. package/test/standard-coverage.test.ts +94 -0
  325. package/test/state-axes.test.ts +1 -1
  326. package/test/streamline-e2e.test.ts +14 -14
  327. package/test/structure-revision-db.test.ts +11 -11
  328. package/test/tenant-registry-db.test.ts +5 -5
  329. package/test/twin-audit.test.ts +83 -0
  330. package/test/twin-model-item-db.test.ts +275 -0
  331. package/test/twin-model-tree-db.test.ts +176 -0
  332. package/test/twin-origin-resync.test.ts +114 -0
  333. package/test/warm-start-seam.test.ts +6 -6
  334. package/test/warm-start.test.ts +88 -3
  335. package/tsconfig.json +6 -1
  336. package/tsconfig.shared.json +23 -0
  337. package/tsconfig.shared.tsbuildinfo +1 -0
  338. package/tsconfig.tsbuildinfo +1 -0
  339. package/dist-server/engine/board-vocabulary.js.map +0 -1
  340. package/dist-server/engine/entity-delta.js.map +0 -1
  341. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  342. package/dist-server/engine/twin-level.js.map +0 -1
  343. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  344. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  345. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  346. package/dist-server/tsconfig.tsbuildinfo +0 -1
  347. package/server/service/twin-event/backfill-keys.ts +0 -72
  348. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  350. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  352. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  353. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -3,8 +3,11 @@ import { Arg, Ctx, Mutation, Query, Resolver, Directive} from 'type-graphql'
3
3
  import { ScalarObject, getRepository } from '@things-factory/shell'
4
4
 
5
5
  import { TwinEngine } from '../../engine/index.js'
6
+ import { readBoardEquipment } from '@operato/twin-kernel'
7
+
6
8
  import { applyKnobDefaults, filledKnobKeys } from './knob-defaults.js'
7
- import { knobDefaultsFilled } from './reference-master.js'
9
+ import type { KnobDef } from './template-registry.js'
10
+ import { knobDefaultsFilled, pickOriginSite, siteEquipmentAdopted } from './reference-master.js'
8
11
  import { TwinInstance } from '../twin-instance/twin-instance.js'
9
12
  import { TwinReference } from './twin-reference.js'
10
13
  import { listReferences, getReference } from './reference-registry.js'
@@ -13,6 +16,7 @@ import {
13
16
  discovered, failed, detailOf,
14
17
  referenceNotFound, adapterUnknown, discoveryFailed, discoveryTimedOut
15
18
  } from './discovery-result.js'
19
+ import { refuseImportSpace } from './ingest-space.js'
16
20
  import { startReferenceLiveFeed } from './reference-live.js'
17
21
  import { listTemplates, getTemplate } from './template-registry.js'
18
22
 
@@ -24,7 +28,7 @@ import { listTemplates, getTemplate } from './template-registry.js'
24
28
  /**
25
29
  * 발견 조회의 시간 상한 — 15초.
26
30
  *
27
- * 어댑터 계약에 시간 제한이 없다. 주소가 틀린 엔드포인트는 답하지 않고, 그러면 화면이 무한히 돈다
31
+ * 어댑터 계약에 시간 제한이 없다. 주소가 틀린 엔드포인트는 답하지 않고, 그러면 화면이 무한히 로딩된다
28
32
  * 사용자에게는 "실패" 보다 못한 상태다(무엇을 기다리는지 모른 채 기다린다). 실 시스템 조회가 몇 초
29
33
  * 걸리는 것은 정상이므로 넉넉히 두되, 끝은 있어야 한다.
30
34
  */
@@ -41,6 +45,76 @@ function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
41
45
  })
42
46
  }
43
47
 
48
+ /**
49
+ * 거절을 **사용자 언어로 옮겨 보낸다** — 번역은 한 곳에서 한다.
50
+ *
51
+ * ── 왜 서버인가 (2026-08-14) ────────────────────────────────────────────────
52
+ * 실측: 실행 중인 트윈을 다시 프로비저닝하면 「instance "order-check" is running — stop before
53
+ * re-provisioning」 이라는 **영어 한 줄이 토스트로** 떴다(다섯 언어 제품에서).
54
+ *
55
+ * 서버와 앱은 **같은 사전**을 쓰고, 사용자의 현재 언어가 백엔드에 반영된다. 그래서 이 문구는 서버가
56
+ * 옮기는 것이 맞다 — 프레임워크가 이미 같은 규약이다(`authenticate-401-middleware` 가
57
+ * `context.t('error.' + errorCode, detail)` 로 옮긴다).
58
+ *
59
+ * ── 그러면 코드는 왜 함께 보내나 ────────────────────────────────────────────
60
+ * 화면이 **구조로** 다뤄야 하는 거절이 따로 있다(정합성 표의 행·심각도·수리 버튼 — 그것들은
61
+ * `twin.compose.chk.*` 처럼 화면이 옮긴다). 그리고 브라우저가 아닌 소비자(AI·스크립트·API)에게는
62
+ * 코드가 계약이다. 문장과 코드는 서로를 대신하지 않는다.
63
+ *
64
+ * **두 곳에서 같은 코드를 옮기지 않는다** — 문장이 두 벌이 되면 반드시 어긋난다.
65
+ */
66
+ function refusal(context: ResolverContext, e: any, fallback: string): Record<string, unknown> {
67
+ const code = typeof e?.code === 'string' ? e.code : undefined
68
+ const params = e?.params && typeof e.params === 'object' ? e.params : undefined
69
+ /* 영어 canonical 은 항상 남긴다 — 번역 키가 없으면 그것이 보이고, 로그에는 그것이 남는다. */
70
+ const canonical = e?.message || fallback
71
+ const message =
72
+ code && typeof context.t === 'function'
73
+ ? String(context.t(`twin.provision.err.${code}`, { ...(params ?? {}), defaultValue: canonical }))
74
+ : canonical
75
+ return { ok: false, error: message, ...(code ? { errorCode: code } : {}), ...(params ? { errorParams: params } : {}) }
76
+ }
77
+
78
+ /**
79
+ * `siteEquipmentIds` 노브를 이 현장의 설비로 채운다 — **비어 있을 때만.**
80
+ *
81
+ * 사용자가 목록을 직접 준 경우에는 손대지 않는다(고른 것이 정본이다). 현장이 없으면 채울 것도 없다.
82
+ * 등록부의 모델에서 읽는다(런타임이 아니다) — 트윈이 도는 중이 아니어도 그 현장의 설비는 사실이다.
83
+ */
84
+ async function resolveSiteEquipmentKnobs(
85
+ knobs: readonly KnobDef[] | undefined,
86
+ effective: Record<string, any>,
87
+ domainId: string,
88
+ spaceId?: string
89
+ ): Promise<{ key: string; count: number }[]> {
90
+ const targets = (knobs ?? []).filter(k => k?.type === 'siteEquipment')
91
+ if (!targets.length) return []
92
+ const out: { key: string; count: number }[] = []
93
+ let ids: { id: string; kind: string }[] | undefined
94
+ for (const k of targets) {
95
+ const given = effective[k.key]
96
+ if (Array.isArray(given) && given.length) continue // 사용자가 고른 것이 정본이다
97
+ if (!spaceId) {
98
+ effective[k.key] = []
99
+ continue
100
+ }
101
+ if (ids === undefined) {
102
+ const rows = await getRepository(TwinInstance).find({ where: { domain: { id: domainId }, spaceId } })
103
+ const seen = new Map<string, { id: string; kind: string }>()
104
+ for (const r of rows) {
105
+ for (const e of readBoardEquipment((r.model as any) ?? {})) {
106
+ if (!e?.id || seen.has(String(e.id))) continue
107
+ seen.set(String(e.id), { id: String(e.id), kind: String((e as any).kind ?? '') })
108
+ }
109
+ }
110
+ ids = [...seen.values()]
111
+ }
112
+ effective[k.key] = ids
113
+ out.push({ key: k.key, count: ids.length })
114
+ }
115
+ return out
116
+ }
117
+
44
118
  @Resolver()
45
119
  export class TwinReferenceResolver {
46
120
  /** 등록된 레퍼런스 소스 목록(요약 + 이미 인제스트됐는지). */
@@ -76,12 +150,119 @@ export class TwinReferenceResolver {
76
150
  if (!master) return { ok: false, error: `unknown reference source: ${source}` }
77
151
  const domainId = context.state.domain.id
78
152
  try {
79
- const res = await TwinEngine.ingestMaster(domainId, master)
153
+ /* 어디서 왔는지 남긴다 이것이 없으면 이 트윈은 두 번 다시 읽을 수 없다.
154
+ 이 경로는 **메모리에 등록된 마스터 하나**를 그대로 쓰므로 사이트 서술자가 없다(사이트 정체를
155
+ 적을 것이 없다). 다시 읽기도 같은 등록본을 집으므로 사이트를 찾을 필요가 없다. */
156
+ const res = await TwinEngine.ingestMaster(domainId, { ...master, origin: { kind: 'reference', source } }, undefined, context.state.user)
80
157
  if (start) await TwinEngine.startFromRegistry(domainId, res.instanceId)
81
158
  return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings: res.warnings }
82
159
  } catch (e: any) {
83
- return { ok: false, error: e?.message || 'ingest failed' }
160
+ return refusal(context, e, 'ingest failed')
161
+ }
162
+ }
163
+
164
+ /**
165
+ * **원본에서 다시 읽는다** — 이 트윈의 구조를 지금의 원본 모습으로 갱신한다.
166
+ *
167
+ * ── 왜 진입점이 하나여야 하나 ──────────────────────────────────────────────
168
+ * 지금까지 다시 읽기는 **만드는 경로에 얹혀 있었다**(`importFromReference`·`createFromTemplate`).
169
+ * 그래서 이미 있는 트윈을 갱신하려면 만들 때 무엇을 골랐는지 사람이 기억해 같은 화면을 다시
170
+ * 거쳐야 했고, 템플릿으로 세운 트윈은 노브를 어디에도 안 남겨서 **아예 갱신할 방법이 없었다.**
171
+ * 원천이 무엇이든 트윈 자신이 기억하게 하고(`TwinInstance.origin`), 여기 한 곳에서 그것을 따른다.
172
+ *
173
+ * ── 무엇이 일어나나 ────────────────────────────────────────────────────────
174
+ * 원본에서 마스터를 다시 얻어 인제스트에 그대로 태운다. 그다음은 인제스트가 아는 일이다 —
175
+ * 구조가 달라졌으면 새 리비전이 남고, 구조 행이 다시 그려지고, **실행 중인 미러는 멈추지 않고
176
+ * 전환한다.** 실행 중인 시뮬레이션은 거절된다(조건이 바뀐 실험은 다른 실험이다).
177
+ *
178
+ * ── 못 할 때는 못 한다고 한다 ──────────────────────────────────────────────
179
+ * `origin` 이 없는 트윈(이 자리가 생기기 전에 만들어진 것)은 다시 읽을 수 없다. 짐작으로
180
+ * 템플릿을 고르거나 첫 레퍼런스를 집지 않는다 — 엉뚱한 원본으로 현장을 덮는 것이 훨씬 나쁘다.
181
+ */
182
+ @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
183
+ @Mutation(returns => ScalarObject, {
184
+ description:
185
+ "Re-read a twin's structure from the source it remembers (TwinInstance.origin). Works for both reference- and template-born twins. A running mirror adopts the new structure without stopping; a running simulation is refused."
186
+ })
187
+ async resyncTwin(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {
188
+ const domainId = context.state.domain.id
189
+ const inst = await getRepository(TwinInstance).findOne({ where: { domain: { id: domainId }, instanceId } })
190
+ if (!inst) return { ok: false, error: `unknown twin: ${instanceId}` }
191
+
192
+ const origin = inst.origin as any
193
+ if (!origin?.kind)
194
+ return {
195
+ ok: false,
196
+ error: `twin "${instanceId}" does not remember where it came from — it cannot be re-read. Re-create it from its template or reference so the origin is recorded.`
197
+ }
198
+
199
+ try {
200
+ const master = await this.masterFromOrigin(domainId, origin, instanceId)
201
+ /*
202
+ * **어느 현장의 트윈이었나를 지킨다** — 다시 읽기는 구조만 새로 그리는 일이고, 현장 소속은 그때
203
+ * 사람이 고른 것이다. 기억이 없는 옛 트윈은 **지금 있는 현장**을 그대로 쓴다(빈손으로 두면 템플릿
204
+ * 기본 공간으로 옮겨 간다 — 조용히 현장을 떠나고 그 현장의 값도 잃는다).
205
+ */
206
+ const keepSpace = origin.spaceId || inst.spaceId
207
+ const res = await TwinEngine.ingestMaster(domainId, master, keepSpace ? { spaceId: keepSpace } : undefined, context.state.user)
208
+ return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings: res.warnings }
209
+ } catch (e: any) {
210
+ return refusal(context, e, 'resync failed')
211
+ }
212
+ }
213
+
214
+ /** 기억해 둔 원천에서 마스터를 다시 얻는다 — 어댑터든 템플릿이든 이 한 곳에서 갈린다. */
215
+ private async masterFromOrigin(domainId: string, origin: any, instanceId: string): Promise<any> {
216
+ if (origin.kind === 'template') {
217
+ const tpl = getTemplate(origin.templateId)
218
+ if (!tpl) throw new Error(`unknown template: ${origin.templateId} — this twin cannot be rebuilt`)
219
+ /* 저장해 둔 노브 그대로 다시 짓는다. 기본값을 다시 입히지 않는다 — 그 사이 템플릿의 기본값이
220
+ 바뀌었다면, 사용자가 고르지 않은 값이 조용히 현장을 바꾸게 된다. */
221
+ const { master } = tpl.build(origin.params ?? {})
222
+ master.origin = origin
223
+ return master
84
224
  }
225
+
226
+ /* 레퍼런스 — 메모리 등록본이 있으면 그것, 없으면 저장된 어댑터 연결로 다시 가져온다.
227
+ (부팅 뒤에는 메모리 등록본이 비어 있으므로 어댑터 경로가 실제로 쓰이는 길이다.) */
228
+ const inMemory = getReference(origin.source)
229
+ if (inMemory) return { ...inMemory, origin }
230
+
231
+ const ref = await getRepository(TwinReference).findOne({ where: { domain: { id: domainId }, source: origin.source } })
232
+ if (!ref) throw new Error(`reference "${origin.source}" is no longer registered — nothing to re-read from`)
233
+ const adapter = getAdapter(ref.adapterType)
234
+ if (!adapter) throw new Error(`no adapter for type: ${ref.adapterType}`)
235
+ const cfg = ref.connectionConfig ?? {}
236
+ const sites = await adapter.discoverSites(cfg)
237
+
238
+ /*
239
+ * ── 무엇을 무엇과 견주나 (2026-08-18 실측으로 고침) ────────────────────────
240
+ *
241
+ * 예전에는 `master.source === origin.source` 로 사이트를 찾았다. 그런데 `origin.source` 는 **레퍼런스
242
+ * 등록 이름**이고(그 참조의 사이트 전부에 같다) `master.source` 는 **사이트별 인스턴스 id** 다 —
243
+ * 서로 다른 것을 견줬으니 사이트가 멀쩡히 있어도 「더 이상 보고하지 않는다」로 거절했다. 사이트가
244
+ * 여럿인 참조에서 온 트윈은 그래서 **전부** 재동기가 불가했고, 오류 문장은 원인을 참조 쪽으로
245
+ * 잘못 가리켰다.
246
+ *
247
+ * 이제 ① 기억해 둔 사이트로 곧장 간다(`origin.siteId`) ② 기억이 없는 옛 트윈은 사이트를 살펴
248
+ * **마스터의 정체(=이 인스턴스 id)** 와 견준다. 어느 쪽도 못 찾으면 **첫 사이트로 때우지 않는다** —
249
+ * 다른 현장의 구조를 이 트윈에 덮는 것은 오류가 아니라 조용히 틀린 공장으로 나타난다.
250
+ */
251
+ const picked = pickOriginSite(sites, origin)
252
+ if (picked.site) return { ...(await adapter.fetchMaster(cfg, picked.site)), origin }
253
+ if (picked.reason === 'site-gone')
254
+ throw new Error(
255
+ `reference "${origin.source}" no longer reports site "${origin.siteId}" — refusing to overwrite this twin with a different site`
256
+ )
257
+
258
+ for (const site of sites) {
259
+ const master = await adapter.fetchMaster(cfg, site)
260
+ if (master.source === instanceId) return { ...master, origin: { ...origin, siteId: site.siteId } }
261
+ }
262
+ throw new Error(
263
+ `reference "${origin.source}" does not report a site that rebuilds twin "${instanceId}" ` +
264
+ `(this twin was imported before the site identity was recorded, and no site's master carries its id) — refusing to overwrite it with a different site`
265
+ )
85
266
  }
86
267
 
87
268
  /** 등록된 트윈 템플릿(파라미터화 아키타입) 목록 — 노브 스키마 포함(클라이언트 폼용). */
@@ -112,10 +293,24 @@ export class TwinReferenceResolver {
112
293
  */
113
294
  const filled = filledKnobKeys(tpl.knobs, params)
114
295
  const effective = applyKnobDefaults(tpl.knobs, params)
296
+ /*
297
+ * **이 현장에 이미 있는 설비를 노브 값으로 확정한다** (`siteEquipmentIds`).
298
+ *
299
+ * 에너지 트윈이 「공장이 돌면 전기가 오른다」를 말하려면 부하를 **이웃 트윈의 설비 id 로**
300
+ * 선언해야 한다(설비 하나에 사실 둘). 그 목록을 빌드할 때마다 현장에서 다시 읽으면, 사이에
301
+ * 설비가 늘거나 준 만큼 **다시 지을 때 다른 트윈**이 된다. 그래서 여기서 한 번 해석하고
302
+ * 그 결과를 노브 값으로 남긴다(아래 `origin.params` 에 그대로 저장된다).
303
+ */
304
+ const adopted = await resolveSiteEquipmentKnobs(tpl.knobs, effective, domainId, spaceId)
115
305
  const { master, scenario } = tpl.build(effective)
306
+ /* **무엇으로 지었는지 남긴다.** 합성은 결정적이므로 같은 노브면 같은 마스터가 다시 나온다 —
307
+ 그래서 템플릿 트윈도 어댑터 트윈과 똑같이 다시 읽을 수 있다. 남기지 않으면 그 트윈은
308
+ 첫날의 모습에 영원히 머문다(원본을 잃은 트윈). */
309
+ /* 고른 현장도 남긴다 — 없으면 다시 읽기가 트윈을 템플릿 기본 공간으로 옮겨 버린다(현장 값도 잃는다). */
310
+ master.origin = { kind: 'template', templateId, params: effective, ...(spaceId ? { spaceId } : {}) }
116
311
  /* 공간 선택은 템플릿을 거치지 않는다 — 템플릿마다 이 노브를 알고 있어야 한다면 새 템플릿이
117
312
  조용히 무시하게 된다. 합성된 마스터에 한 곳에서 얹는다(레퍼런스 인제스트와 같은 경로). */
118
- const res = await TwinEngine.ingestMaster(domainId, master, { spaceId })
313
+ const res = await TwinEngine.ingestMaster(domainId, master, { spaceId }, context.state.user)
119
314
  if (start !== false) {
120
315
  await TwinEngine.startFromRegistry(domainId, res.instanceId)
121
316
  if (scenario) {
@@ -126,10 +321,14 @@ export class TwinReferenceResolver {
126
321
  }
127
322
  }
128
323
  }
129
- const warnings = filled.length ? [knobDefaultsFilled(filled), ...res.warnings] : res.warnings
324
+ const warnings = [
325
+ ...(filled.length ? [knobDefaultsFilled(filled)] : []),
326
+ ...adopted.filter(a => a.count > 0).map(a => siteEquipmentAdopted(a.key, a.count)),
327
+ ...res.warnings
328
+ ]
130
329
  return { ok: true, instanceId: res.instanceId, spaceId: res.spaceId, warnings }
131
330
  } catch (e: any) {
132
- return { ok: false, error: e?.message || 'template instantiation failed' }
331
+ return refusal(context, e, 'template instantiation failed')
133
332
  }
134
333
  }
135
334
 
@@ -309,7 +508,7 @@ export class TwinReferenceResolver {
309
508
  @Query(returns => ScalarObject, { description: 'Discover sites/factories exposed by a reference. Returns { sites, error? } — an empty list with no error means the system genuinely has none, which is different from a failure.' })
310
509
  async discoverReferenceSites(@Arg('source') source: string, @Ctx() context: ResolverContext): Promise<any> {
311
510
  /*
312
- * **"못 찾았다" 와 "없다" 를 가른다.** 예전에는 넷을 빈 배열 하나로 접었고(레퍼런스 없음·어댑터
511
+ * **"못 찾았다" 와 "없다" 를 구분한다.** 예전에는 넷을 빈 배열 하나로 접었고(레퍼런스 없음·어댑터
313
512
  * 미상·조회 실패·정말 0건), 화면은 그 모두를 "사이트를 찾지 못했습니다" 라고만 말했다. 실 시스템
314
513
  * 연결에서 가장 자주 막히는 자리인데 자격이 틀렸는지 주소가 틀렸는지 알 수 없었다.
315
514
  * 판정과 문장 형태는 `discovery-result`(순수)가 갖는다.
@@ -335,15 +534,25 @@ export class TwinReferenceResolver {
335
534
  /**
336
535
  * 트윈 생성 — 레퍼런스에서 사이트 발견 → (siteIds 있으면 그 공장만) → fetchMaster → provision. 사이트별 resilient.
337
536
  * 여러 공장을 관할하는 소스는 siteIds 로 "어느 공장"을 골라 생성(1:N, §2.1·§2.2).
537
+ *
538
+ * ── 현장 선택이 이 경로에도 있다 (2026-08-13) ────────────────────────────
539
+ * 한 현실을 여러 렌즈가 비춘다(WMS·MES·YMS). 그런데 **그 선택이 템플릿 경로에만** 있어서
540
+ * (`createFromTemplate(spaceId)`), 실 시스템에서 가져온 트윈은 어댑터가 파생한 공간 id 가 우연히
541
+ * 일치할 때만 한 현장에 모였다 — 사람이 "이 MES 와 같은 현장에 넣어라" 를 말할 방법이 없었다.
542
+ * 데이터로는 가능한 일이 저작으로는 불가능한, `ingest-space` 가 이미 지적한 그 비대칭이다.
543
+ *
544
+ * 판정은 그 순수 모듈이 갖는다: 없는 공간은 거절(`resolveIngestSpace`), 사이트 여럿을 한 공간에
545
+ * 넣는 것도 거절(`refuseImportSpace`) — 서로 다른 현장이 조용히 합쳐지는 것을 막는다.
338
546
  */
339
547
  @Directive('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)')
340
- @Mutation(returns => ScalarObject, { description: 'Create twins from a reference: discover sites → filter by siteIds (if given) → fetch master(s) → provision. start=false to only provision; live=true to run as a mirror (adapter openLiveFeed → live projector) instead of sim.' })
548
+ @Mutation(returns => ScalarObject, { description: 'Create twins from a reference: discover sites → filter by siteIds (if given) → fetch master(s) → provision. spaceId co-locates the twin into an existing space (one site only). start=false to only provision; live=true to run as a mirror (adapter openLiveFeed → live projector) instead of sim.' })
341
549
  async importTwinReference(
342
550
  @Arg('source') source: string,
343
551
  @Ctx() context: ResolverContext,
344
552
  @Arg('siteIds', type => [String], { nullable: true }) siteIds?: string[],
345
553
  @Arg('start', { nullable: true }) start?: boolean,
346
- @Arg('live', { nullable: true }) live?: boolean
554
+ @Arg('live', { nullable: true }) live?: boolean,
555
+ @Arg('spaceId', { nullable: true, description: 'Existing space to co-locate this twin into (add a lens to a site). Only when importing a single site. Omit to give each site its own space.' }) spaceId?: string
347
556
  ): Promise<any> {
348
557
  const domainId = context.state.domain.id
349
558
  const repo = getRepository(TwinReference)
@@ -355,12 +564,22 @@ export class TwinReferenceResolver {
355
564
  try {
356
565
  let sites = await adapter.discoverSites(cfg)
357
566
  if (siteIds?.length) sites = sites.filter(s => siteIds.includes(s.siteId))
567
+ /* 아무것도 쓰기 전에 거절한다 — 한 사이트를 넘으면 "이 현장에 더한다" 가 성립하지 않는다. */
568
+ const refusal = refuseImportSpace(sites.length, spaceId)
569
+ if (refusal) return { ok: false, error: refusal.message, errorCode: refusal.code, errorParams: refusal.params }
358
570
  const instances: any[] = []
359
571
  const failed: any[] = []
360
572
  for (const site of sites) {
361
573
  try {
362
574
  const master = await adapter.fetchMaster(cfg, site)
363
- const res = await TwinEngine.ingestMaster(domainId, master)
575
+ /* 어느 레퍼런스의 **어느 사이트**에서 왔는지 남긴다 — 다시 읽기가 같은 어댑터·같은 사이트로 돌아오려면
576
+ 사이트 정체가 있어야 한다(`source` 는 참조 이름이라 사이트 전부에 같다). */
577
+ const res = await TwinEngine.ingestMaster(
578
+ domainId,
579
+ { ...master, origin: { kind: 'reference', source, siteId: site.siteId, ...(spaceId ? { spaceId } : {}) } },
580
+ { spaceId },
581
+ context.state.user
582
+ )
364
583
  if (start !== false) {
365
584
  // live=true + 어댑터가 openLiveFeed 지원 → mirror(실 이벤트 미러). 아니면 sim 기동(fallback).
366
585
  const wentLive = live ? await startReferenceLiveFeed(domainId, ref.adapterType, cfg, site, res.instanceId) : false
@@ -393,8 +612,9 @@ export class TwinReferenceResolver {
393
612
  )
394
613
  return { ok: instances.length > 0, instances, failed }
395
614
  } catch (e: any) {
615
+ /* 레퍼런스 행에는 **영어 canonical** 을 남긴다 — 저장되는 값은 언어중립이어야 다시 읽을 수 있다. */
396
616
  await repo.update({ id: ref.id }, { status: 'error', lastError: e?.message ?? 'ingest failed' } as any)
397
- return { ok: false, error: e?.message ?? 'ingest failed' }
617
+ return refusal(context, e, 'ingest failed')
398
618
  }
399
619
  }
400
620
  }
@@ -2,8 +2,10 @@
2
2
  * 트윈 템플릿(아키타입) 레지스트리 — 파라미터화된 합성 레퍼런스 마스터 빌더.
3
3
  * 레퍼런스(reference-registry)가 "고정 마스터"라면, 템플릿은 "노브로 파라미터화된 마스터 빌더".
4
4
  * 둘 다 같은 인제스트 경로(ingestMaster)로 흐른다 — 원천이 고정이냐 합성이냐만 다르다(board-authoring §10).
5
- * 앱(operato-twin)이 bootstrap 에서 자기 아키타입을 registerTemplate 한다. 표현(map/image/board) 무관.
5
+ * 앱(operato-twin)이 bootstrap 에서 자기 아키타입을 registerTemplate 한다. 표현(map/image/model) 무관.
6
6
  */
7
+ import type { DomainSystem } from '@operato/twin-kernel'
8
+
7
9
  import type { ReferenceMaster } from './reference-master.js'
8
10
 
9
11
  /** 사용자 노출 파라미터(노브) 정의 — 클라이언트가 이 스키마로 폼을 렌더한다. */
@@ -11,7 +13,14 @@ export interface KnobDef {
11
13
  key: string
12
14
  /** 'geo' = 위경도 지점(지도에서 클릭 선택). 값은 { lat, lon }. 사용자가 좌표를 몰라도 지도로 지정. */
13
15
  label: string
14
- type: 'number' | 'text' | 'select' | 'geo'
16
+ /**
17
+ * 'siteEquipment' = **이 현장에 이미 있는 설비들**(`{id, kind}`). 값이 비어 있으면 만들 때 한 번
18
+ * 해석해서 채우고, 채운 결과를 노브 값으로 **남긴다**(`origin.params`). 그래야 나중에 다시 지어도
19
+ * 같은 트윈이 나온다 — 그때그때 현장을 다시 읽으면 사이에 설비가 늘고 준 만큼 다른 트윈이 된다.
20
+ *
21
+ * 종류를 함께 싣는 이유: 무엇을 받아들일지는 **템플릿이 종류로 판단**한다(이름으로 짐작하지 않는다).
22
+ */
23
+ type: 'number' | 'text' | 'select' | 'geo' | 'siteEquipment'
15
24
  default: any
16
25
  min?: number
17
26
  max?: number
@@ -40,8 +49,13 @@ export interface TemplateDetail {
40
49
  /** 완성 라우트 이름(공정 라우트 참조). */
41
50
  route?: string
42
51
  }
43
- /** 표준 정합 매핑(개념 ↔ EPCIS/ISA-95). */
44
- standardsMap?: { concept: string; epcis?: string; isa95?: string }[]
52
+ /**
53
+ * 표준 정합 매핑 개념이 **어느 표준의 무엇에 대응하나.**
54
+ *
55
+ * 표준 칸이 EPCIS·ISA-95 둘로 굳어 있었다. 에너지 템플릿은 그 둘이 앵커가 아니라 적을 자리가 없었다 —
56
+ * 커널의 `StandardClass` 도 같은 이유로 열었다(`iso50001`·`iec61850`). 대응할 이름이 없으면 **비운다**.
57
+ */
58
+ standardsMap?: { concept: string; epcis?: string; isa95?: string; iso50001?: string; iec61850?: string }[]
45
59
  /** "이 템플릿이 맞나?" 판단 도움 항목. */
46
60
  fits?: string[]
47
61
  }
@@ -49,7 +63,8 @@ export interface TemplateDetail {
49
63
  export interface TwinTemplate {
50
64
  id: string
51
65
  label: string
52
- system: 'wms' | 'yms' | 'mes'
66
+ /** 커널 카탈로그가 정본 종류가 늘면 여기 손댈 것이 없다(`DomainSystem`). */
67
+ system: DomainSystem
53
68
  description?: string
54
69
  /** 상세 설명(선택) — 설명 페이지/패널이 렌더. */
55
70
  detail?: TemplateDetail
@@ -0,0 +1,81 @@
1
+ /*
2
+ * 커맨드 감사 기록 만들기 (순수) — **누가 무엇을 허락했고, 그래서 어떻게 됐나.**
3
+ *
4
+ * 승인 게이트가 앞을 막고, 이 기록이 뒤를 설명한다. 둘 다 있어야 "사람 손을 거쳤다" 를 증명할 수 있다.
5
+ *
6
+ * 무엇을 믿는가가 이 기록의 전부다:
7
+ * · **신원·시각은 서버가 준다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 아무것도
8
+ * 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 쓴다.
9
+ * · **의도는 클라이언트가 준다** — 출처(origin)·근거(reason). 서버가 알 수 없는 정보이고,
10
+ * 위조되더라도 신원 증명을 훼손하지 않는다.
11
+ */
12
+
13
+ export type CommandOrigin = 'user' | 'ai' | 'automation' | 'unknown'
14
+
15
+ export type CommandAuditInput = {
16
+ /** 클라이언트가 실어 보낸 커맨드(신뢰하지 않는 부분을 포함한 원본). */
17
+ command: any
18
+ /** 커널 판정. */
19
+ ack: { accepted?: boolean; errorCode?: string; error?: string }
20
+ /** 세션이 아는 사용자 — **서버만 채운다.** */
21
+ actor?: { id?: string; email?: string; name?: string }
22
+ /** 서버 시각. 호출부가 주입해 테스트가 시간을 고정할 수 있게 한다. */
23
+ at: Date
24
+ }
25
+
26
+ /** `TwinAuditEvent` 에 그대로 넣을 수 있는 모양(엔티티를 몰라도 되게 순수하게 둔다). */
27
+ export type CommandAuditRow = {
28
+ eventTime: Date
29
+ commandId?: string
30
+ commandType?: string
31
+ origin: CommandOrigin
32
+ reason?: string
33
+ actorId?: string
34
+ actorEmail?: string
35
+ actorName?: string
36
+ accepted: boolean
37
+ errorCode?: string
38
+ args?: any
39
+ argsTruncated: boolean
40
+ }
41
+
42
+ /** 신뢰할 수 있는 출처 표기로 정규화 — 모르는 값은 지어내지 않고 'unknown' 으로 둔다. */
43
+ export function normalizeOrigin(v: unknown): CommandOrigin {
44
+ return v === 'user' || v === 'ai' || v === 'automation' ? v : 'unknown'
45
+ }
46
+
47
+ /** args 직렬화 상한(문자). 넘으면 자르되 잘랐다고 표시한다. */
48
+ const ARGS_MAX = 4000
49
+
50
+ export function buildCommandAudit(input: CommandAuditInput): CommandAuditRow {
51
+ const { command, ack, actor, at } = input
52
+ const argsText = safeJson(command?.args)
53
+ const truncated = argsText !== null && argsText.length > ARGS_MAX
54
+ return {
55
+ eventTime: at,
56
+ commandId: str(command?.commandId),
57
+ commandType: str(command?.type),
58
+ origin: normalizeOrigin(command?.origin),
59
+ reason: str(command?.reason),
60
+ /* 신원 — 세션 값만. 커맨드에 실려 온 신원 주장은 쳐다보지 않는다. */
61
+ actorId: str(actor?.id),
62
+ actorEmail: str(actor?.email),
63
+ actorName: str(actor?.name),
64
+ /* 결과 — 승인했는데 커널이 거절한 것도 감사 대상이다(무엇을 시도했는지가 남아야 한다). */
65
+ accepted: !!ack?.accepted,
66
+ errorCode: str(ack?.errorCode),
67
+ /* 인자는 원형 그대로. 너무 크면 잘린 문자열로 두되 잘랐다고 말한다. */
68
+ args: truncated ? argsText!.slice(0, ARGS_MAX) : command?.args ?? undefined,
69
+ argsTruncated: truncated
70
+ }
71
+ }
72
+
73
+ const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined)
74
+
75
+ function safeJson(v: unknown): string | null {
76
+ try {
77
+ return v === undefined ? null : JSON.stringify(v)
78
+ } catch {
79
+ return null /* 순환 참조 등 — 못 재면 자르지 않는다(원본을 그대로 둔다) */
80
+ }
81
+ }
@@ -0,0 +1,5 @@
1
+ import { TwinAuditEvent } from './twin-audit-event.js'
2
+ import { TwinAuditQuery } from './twin-audit-query.js'
3
+
4
+ export const entities = [TwinAuditEvent]
5
+ export const resolvers = [TwinAuditQuery]
@@ -0,0 +1,112 @@
1
+ import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
2
+ import { ObjectType, Field, ID } from 'type-graphql'
3
+
4
+ import { Domain, ScalarObject } from '@things-factory/shell'
5
+
6
+ /*
7
+ * TwinAuditEvent — **사람이 무엇을 허락했는가.**
8
+ *
9
+ * ── 왜 저널(TwinEvent)이 아닌가 ───────────────────────────────────────────────
10
+ * 처음에는 저널에 함께 넣었다. "감사는 원장에서 하니까" 라는 이유였는데, 그건 **화면의 편의**였고
11
+ * 저장 구조를 정할 근거가 못 됐다. 넣자마자 예외가 넷 생겼다 — 리플레이에서 빼고, 백필에서 빼고,
12
+ * 원장 거래 분류에서 빼고, 원장 렌더에서 갈랐다. **예외가 넷이면 설계가 "여기 아니다" 라고 말하는 것이다.**
13
+ *
14
+ * 갈라 두면 예외가 0 이고, 오히려 얻는다:
15
+ * · 행위자가 **컬럼**이 된다 — "누가 승인한 것 전부" 는 감사의 기본 질문인데 payload 안에서는 못 찾았다
16
+ * · revision 이 없다. 저널에서는 그 칸을 비워 "상태가 아님" 을 뜻하게 썼는데, 없음을 종류 표시로
17
+ * 쓰는 것이 여러 번 문제를 냈다(백필의 `bizStep IS NULL = 옛 행` 전제가 이것 때문에 깨졌다)
18
+ * · bizStep 오염이 없다 — 저널의 키 추출기는 값이 없으면 이벤트 타입으로 대체하므로,
19
+ * 감사 행을 태우면 CBV 표준 어휘 컬럼에 `governance.command` 가 박힌다
20
+ *
21
+ * ── 무엇을 믿는가 ────────────────────────────────────────────────────────────
22
+ * **신원·시각은 서버가 각인한다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 그 기록은
23
+ * 아무것도 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 신뢰한다.
24
+ * 클라이언트에서 받는 것은 **의도**뿐이다 — 어디서 시작됐나(origin), 왜 하나(reason).
25
+ */
26
+ @Entity()
27
+ /* 목록 기본 정렬 — 최근 것부터. 감사는 "요즘 무슨 일이 있었나" 로 시작한다. */
28
+ @Index('ix_twin_audit_0', (e: TwinAuditEvent) => [e.domain, e.instanceId, e.eventTime], { unique: false })
29
+ /* 행위자로 찾기 — 저널에 있을 때는 불가능했던 질문("김현장이 승인한 것 전부"). */
30
+ @Index('ix_twin_audit_1', (e: TwinAuditEvent) => [e.domain, e.actorEmail, e.eventTime], { unique: false })
31
+ @ObjectType({ description: 'Audit record of a twin command: who approved what, and what the kernel answered.' })
32
+ export class TwinAuditEvent {
33
+ @PrimaryGeneratedColumn('uuid')
34
+ @Field(type => ID, { description: 'Unique identifier of the audit record.' })
35
+ readonly id: string
36
+
37
+ @ManyToOne(type => Domain)
38
+ @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })
39
+ domain?: Domain
40
+
41
+ @RelationId((e: TwinAuditEvent) => e.domain)
42
+ domainId?: string
43
+
44
+ @Column()
45
+ @Field({ description: 'Twin runtime instance the command was aimed at.' })
46
+ instanceId: string
47
+
48
+ /*
49
+ * 서버 시각. 클라이언트 시계를 믿지 않는다 — 기록의 시간축이 단말 설정에 좌우되면 안 된다.
50
+ *
51
+ * `datetime` 을 쓴다. `timestamp` 는 **sqlite 가 지원하지 않아** 5개 드라이버 공통이 아니다
52
+ * (실제로 그렇게 썼다가 DB 를 쓰는 검사 33개가 한꺼번에 터졌다).
53
+ */
54
+ @Column({ type: 'datetime' })
55
+ @Field({ description: 'Server time when the command was dispatched.' })
56
+ eventTime: Date
57
+
58
+ @Column({ nullable: true })
59
+ @Field({ nullable: true, description: 'Client-supplied command id, for correlating with kernel logs.' })
60
+ commandId?: string
61
+
62
+ @Column({ nullable: true })
63
+ @Field({ nullable: true, description: 'Command type (e.g. order.hold, equipment.repair).' })
64
+ commandType?: string
65
+
66
+ /* 'user' | 'ai' | 'automation' | 'unknown'. 모르면 unknown 이지 사람이 한 것으로 치지 않는다. */
67
+ @Column({ nullable: true })
68
+ @Field({ nullable: true, description: "Where the command originated: 'user' | 'ai' | 'automation' | 'unknown'." })
69
+ origin?: string
70
+
71
+ @Column({ type: 'text', nullable: true })
72
+ @Field({ nullable: true, description: 'Why this action — the rationale shown on the approval card.' })
73
+ reason?: string
74
+
75
+ /* ── 신원: 세션에서만 온다 ── */
76
+ @Column({ nullable: true })
77
+ @Field({ nullable: true, description: 'Approving user id, stamped by the server from the session.' })
78
+ actorId?: string
79
+
80
+ @Column({ nullable: true })
81
+ @Field({ nullable: true, description: 'Approving user email, stamped by the server from the session.' })
82
+ actorEmail?: string
83
+
84
+ @Column({ nullable: true })
85
+ @Field({ nullable: true, description: 'Approving user name, stamped by the server from the session.' })
86
+ actorName?: string
87
+
88
+ /* ── 결과: 거절도 남긴다 ── */
89
+ @Column({ default: false })
90
+ @Field({ description: 'Whether the kernel accepted the command.' })
91
+ accepted: boolean
92
+
93
+ @Column({ nullable: true })
94
+ @Field({ nullable: true, description: 'Language-neutral rejection code when the kernel refused.' })
95
+ errorCode?: string
96
+
97
+ /**
98
+ * 명령 인자 — **원형 그대로.** 무엇에 대한 명령이었는지가 감사의 핵심이라 요약하지 않는다.
99
+ * 지나치게 크면 자르되 `argsTruncated` 로 잘랐다는 사실을 남긴다(조용히 버리지 않는다).
100
+ */
101
+ @Column({ type: 'simple-json', nullable: true })
102
+ @Field(type => ScalarObject, { nullable: true, description: 'Command arguments as given.' })
103
+ args?: any
104
+
105
+ @Column({ default: false })
106
+ @Field({ description: 'True when args were too large and stored truncated.' })
107
+ argsTruncated: boolean
108
+
109
+ @CreateDateColumn()
110
+ @Field({ nullable: true, description: 'Timestamp when the audit row was written.' })
111
+ createdAt?: Date
112
+ }