@things-factory/headless-twin 10.0.9 → 10.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  2. package/dist-server/engine/canonical-ingest.js +24 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/energy-topology.d.ts +86 -0
  7. package/dist-server/engine/energy-topology.js +144 -0
  8. package/dist-server/engine/energy-topology.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +8 -3
  10. package/dist-server/engine/index.js +10 -3
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/kpi-baseline.d.ts +1 -1
  13. package/dist-server/engine/kpi-baseline.js.map +1 -1
  14. package/dist-server/engine/kpi-fold.d.ts +137 -1
  15. package/dist-server/engine/kpi-fold.js +225 -0
  16. package/dist-server/engine/kpi-fold.js.map +1 -1
  17. package/dist-server/engine/kpi-query.d.ts +1 -1
  18. package/dist-server/engine/kpi-query.js +212 -18
  19. package/dist-server/engine/kpi-query.js.map +1 -1
  20. package/dist-server/engine/kpi-target.d.ts +17 -0
  21. package/dist-server/engine/kpi-target.js +24 -2
  22. package/dist-server/engine/kpi-target.js.map +1 -1
  23. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  24. package/dist-server/engine/live-feed-registry.js +51 -0
  25. package/dist-server/engine/live-feed-registry.js.map +1 -0
  26. package/dist-server/engine/load-meter.d.ts +181 -0
  27. package/dist-server/engine/load-meter.js +267 -0
  28. package/dist-server/engine/load-meter.js.map +1 -0
  29. package/dist-server/engine/local-declarations.d.ts +262 -0
  30. package/dist-server/engine/local-declarations.js +528 -0
  31. package/dist-server/engine/local-declarations.js.map +1 -0
  32. package/dist-server/engine/model-basis.d.ts +39 -6
  33. package/dist-server/engine/model-basis.js +69 -9
  34. package/dist-server/engine/model-basis.js.map +1 -1
  35. package/dist-server/engine/{board-vocabulary.d.ts → model-vocabulary.d.ts} +4 -4
  36. package/dist-server/engine/{board-vocabulary.js → model-vocabulary.js} +37 -19
  37. package/dist-server/engine/model-vocabulary.js.map +1 -0
  38. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  39. package/dist-server/engine/oee-accumulator.js +26 -1
  40. package/dist-server/engine/oee-accumulator.js.map +1 -1
  41. package/dist-server/engine/operation-basis.d.ts +32 -0
  42. package/dist-server/engine/operation-basis.js +87 -0
  43. package/dist-server/engine/operation-basis.js.map +1 -0
  44. package/dist-server/engine/property-effects.d.ts +30 -0
  45. package/dist-server/engine/property-effects.js +192 -0
  46. package/dist-server/engine/property-effects.js.map +1 -0
  47. package/dist-server/engine/runtime-key.d.ts +1 -1
  48. package/dist-server/engine/runtime-key.js +1 -1
  49. package/dist-server/engine/runtime-key.js.map +1 -1
  50. package/dist-server/engine/spec-coverage.js +3 -3
  51. package/dist-server/engine/spec-coverage.js.map +1 -1
  52. package/dist-server/engine/state-axes.d.ts +7 -2
  53. package/dist-server/engine/state-axes.js +8 -3
  54. package/dist-server/engine/state-axes.js.map +1 -1
  55. package/dist-server/engine/structure-diff.js +1 -1
  56. package/dist-server/engine/structure-diff.js.map +1 -1
  57. package/dist-server/engine/travel-estimator.d.ts +9 -2
  58. package/dist-server/engine/travel-estimator.js +11 -6
  59. package/dist-server/engine/travel-estimator.js.map +1 -1
  60. package/dist-server/engine/twin-engine.d.ts +332 -47
  61. package/dist-server/engine/twin-engine.js +1314 -279
  62. package/dist-server/engine/twin-engine.js.map +1 -1
  63. package/dist-server/engine/warm-start.d.ts +38 -4
  64. package/dist-server/engine/warm-start.js +38 -9
  65. package/dist-server/engine/warm-start.js.map +1 -1
  66. package/dist-server/index.d.ts +1 -0
  67. package/dist-server/index.js +17 -11
  68. package/dist-server/index.js.map +1 -1
  69. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  70. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  71. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  72. package/dist-server/migrations/index.d.ts +2 -0
  73. package/dist-server/migrations/index.js +10 -0
  74. package/dist-server/migrations/index.js.map +1 -0
  75. package/dist-server/service/index.d.ts +3 -2
  76. package/dist-server/service/index.js +25 -17
  77. package/dist-server/service/index.js.map +1 -1
  78. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  79. package/dist-server/service/reference/discovery-result.js +2 -2
  80. package/dist-server/service/reference/discovery-result.js.map +1 -1
  81. package/dist-server/service/reference/ingest-space.d.ts +24 -1
  82. package/dist-server/service/reference/ingest-space.js +17 -1
  83. package/dist-server/service/reference/ingest-space.js.map +1 -1
  84. package/dist-server/service/reference/reference-live.d.ts +23 -0
  85. package/dist-server/service/reference/reference-live.js +121 -4
  86. package/dist-server/service/reference/reference-live.js.map +1 -1
  87. package/dist-server/service/reference/reference-master.d.ts +194 -13
  88. package/dist-server/service/reference/reference-master.js +159 -28
  89. package/dist-server/service/reference/reference-master.js.map +1 -1
  90. package/dist-server/service/reference/reference-resolver.d.ts +31 -1
  91. package/dist-server/service/reference/reference-resolver.js +223 -12
  92. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  93. package/dist-server/service/reference/template-registry.d.ts +19 -3
  94. package/dist-server/service/reference/template-registry.js.map +1 -1
  95. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  96. package/dist-server/service/twin-audit/command-audit.js +53 -0
  97. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  98. package/dist-server/service/twin-audit/index.d.ts +4 -0
  99. package/dist-server/service/twin-audit/index.js +8 -0
  100. package/dist-server/service/twin-audit/index.js.map +1 -0
  101. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  102. package/dist-server/service/twin-audit/twin-audit-event.js +141 -0
  103. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  104. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  105. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  106. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  107. package/dist-server/service/twin-control/twin-control-mutation.js +21 -1
  108. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  109. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  110. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  111. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  112. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  113. package/dist-server/service/twin-event/twin-event.js +45 -13
  114. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  115. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  116. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  117. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  118. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  119. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  120. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  121. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  122. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  123. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  124. package/dist-server/service/twin-forecast/twin-forecast-query.js +217 -38
  125. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  126. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  127. package/dist-server/service/twin-instance/twin-instance.js +66 -20
  128. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  129. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -2
  130. package/dist-server/service/twin-journal/twin-journal-query.js +71 -9
  131. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +3 -5
  133. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +24 -27
  134. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  135. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  136. package/dist-server/service/twin-metrics/twin-metrics-query.js +62 -0
  137. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  138. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  139. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  140. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  141. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  142. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  143. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  144. package/dist-server/service/twin-model/index.d.ts +13 -0
  145. package/dist-server/service/twin-model/index.js +29 -0
  146. package/dist-server/service/twin-model/index.js.map +1 -0
  147. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  148. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  149. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  150. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  151. package/dist-server/service/twin-model/project-structure.js +173 -0
  152. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  153. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  154. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  155. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  157. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  158. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  160. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  161. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  162. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  163. package/dist-server/service/twin-model/twin-location.js +130 -0
  164. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  165. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  166. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  167. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  168. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  169. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  170. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  172. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  173. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  176. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  178. package/dist-server/service/twin-model/twin-operation.js +137 -0
  179. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  180. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  181. package/dist-server/service/twin-space/move-space.js +166 -0
  182. package/dist-server/service/twin-space/move-space.js.map +1 -0
  183. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  184. package/dist-server/service/twin-space/space-integrity.js +182 -0
  185. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  186. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  187. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  188. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  189. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  190. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  191. package/dist-server/service/twin-space/twin-space-resolver.js +356 -18
  192. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  193. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  194. package/dist-server/service/twin-space/twin-space.js +15 -5
  195. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  196. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  197. package/dist-server/service/twin-structure/twin-structure.js +8 -8
  198. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  199. package/dist-shared/axis-read.d.ts +12 -0
  200. package/dist-shared/axis-read.js +32 -0
  201. package/dist-shared/axis-read.js.map +1 -0
  202. package/{dist-server/engine → dist-shared}/entity-delta.d.ts +3 -0
  203. package/{dist-server/engine → dist-shared}/entity-delta.js +102 -2
  204. package/dist-shared/entity-delta.js.map +1 -0
  205. package/dist-shared/kpi-broadcast.js.map +1 -0
  206. package/dist-shared/twin-level.js.map +1 -0
  207. package/package.json +14 -12
  208. package/server/engine/canonical-ingest.ts +36 -4
  209. package/server/engine/command-routing.ts +1 -1
  210. package/server/engine/energy-topology.ts +189 -0
  211. package/server/engine/index.ts +10 -3
  212. package/server/engine/kpi-baseline.ts +1 -1
  213. package/server/engine/kpi-fold.ts +389 -1
  214. package/server/engine/kpi-query.ts +214 -19
  215. package/server/engine/kpi-target.ts +24 -2
  216. package/server/engine/live-feed-registry.ts +58 -0
  217. package/server/engine/load-meter.ts +384 -0
  218. package/server/engine/local-declarations.ts +700 -0
  219. package/server/engine/model-basis.ts +78 -10
  220. package/server/engine/{board-vocabulary.ts → model-vocabulary.ts} +36 -15
  221. package/server/engine/oee-accumulator.ts +34 -1
  222. package/server/engine/operation-basis.ts +100 -0
  223. package/server/engine/property-effects.ts +199 -0
  224. package/server/engine/runtime-key.ts +1 -1
  225. package/server/engine/spec-coverage.ts +3 -3
  226. package/server/engine/state-axes.ts +8 -3
  227. package/server/engine/structure-diff.ts +1 -1
  228. package/server/engine/travel-estimator.ts +11 -6
  229. package/server/engine/twin-engine.ts +1414 -274
  230. package/server/engine/warm-start.ts +83 -12
  231. package/server/index.ts +13 -10
  232. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  233. package/server/migrations/index.ts +7 -0
  234. package/server/service/index.ts +9 -1
  235. package/server/service/reference/discovery-result.ts +2 -2
  236. package/server/service/reference/ingest-space.ts +35 -1
  237. package/server/service/reference/reference-live.ts +119 -4
  238. package/server/service/reference/reference-master.ts +276 -35
  239. package/server/service/reference/reference-resolver.ts +232 -12
  240. package/server/service/reference/template-registry.ts +20 -5
  241. package/server/service/twin-audit/command-audit.ts +81 -0
  242. package/server/service/twin-audit/index.ts +5 -0
  243. package/server/service/twin-audit/twin-audit-event.ts +138 -0
  244. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  245. package/server/service/twin-control/twin-control-mutation.ts +22 -1
  246. package/server/service/twin-event/twin-event-keys.ts +1 -1
  247. package/server/service/twin-event/twin-event.ts +59 -14
  248. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  249. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  250. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  251. package/server/service/twin-forecast/twin-forecast-query.ts +208 -36
  252. package/server/service/twin-instance/twin-instance.ts +126 -25
  253. package/server/service/twin-journal/twin-journal-query.ts +74 -10
  254. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +23 -22
  255. package/server/service/twin-metrics/twin-metrics-query.ts +59 -2
  256. package/server/service/twin-model/epcis-coverage.ts +195 -0
  257. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  258. package/server/service/twin-model/index.ts +25 -0
  259. package/server/service/twin-model/isa95-coverage.ts +190 -0
  260. package/server/service/twin-model/project-structure.ts +199 -0
  261. package/server/service/twin-model/standard-coverage.ts +35 -0
  262. package/server/service/twin-model/twin-equipment.ts +157 -0
  263. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  264. package/server/service/twin-model/twin-location.ts +147 -0
  265. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  266. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  267. package/server/service/twin-model/twin-model-query.ts +537 -0
  268. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  269. package/server/service/twin-model/twin-operation.ts +157 -0
  270. package/server/service/twin-space/move-space.ts +262 -0
  271. package/server/service/twin-space/space-integrity.ts +293 -0
  272. package/server/service/twin-space/twin-space-area.ts +4 -4
  273. package/server/service/twin-space/twin-space-representation.ts +6 -6
  274. package/server/service/twin-space/twin-space-resolver.ts +355 -21
  275. package/server/service/twin-space/twin-space.ts +40 -6
  276. package/server/service/twin-structure/twin-structure.ts +10 -10
  277. package/shared/axis-read.ts +28 -0
  278. package/{server/engine → shared}/entity-delta.ts +96 -1
  279. package/test/adopt-structure-live.test.ts +133 -0
  280. package/test/attention-digest.test.ts +3 -3
  281. package/test/axis-read.test.ts +87 -0
  282. package/test/boot-resume.test.ts +205 -0
  283. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  284. package/test/capability-mapping.test.ts +1 -1
  285. package/test/column-type-portability.test.ts +122 -0
  286. package/test/command-routing.test.ts +1 -1
  287. package/test/declaration-reaches-model.test.ts +178 -0
  288. package/test/discovery-result.test.ts +1 -1
  289. package/test/energy-topology.test.ts +113 -0
  290. package/test/entity-delta.test.ts +171 -1
  291. package/test/event-time-column.test.ts +67 -0
  292. package/test/forecast-metrics.test.ts +71 -0
  293. package/test/forecast-tuning.test.ts +42 -0
  294. package/test/gap-analytics.test.ts +34 -7
  295. package/test/ingest-bench.test.ts +6 -6
  296. package/test/ingest-running-guard.test.ts +136 -0
  297. package/test/ingest-space.test.ts +29 -1
  298. package/test/instance-cache-lifecycle.test.ts +110 -0
  299. package/test/kernel-kind-guard.test.ts +96 -0
  300. package/test/kpi-baseline-db.test.ts +3 -1
  301. package/test/kpi-baseline.test.ts +3 -3
  302. package/test/kpi-fold.test.ts +393 -2
  303. package/test/kpi-query-bench.test.ts +5 -3
  304. package/test/lineage-survives-restart.test.ts +203 -0
  305. package/test/live-feed-registry.test.ts +67 -0
  306. package/test/live-kernel-facts.test.ts +161 -0
  307. package/test/live-mirror-parity.test.ts +22 -15
  308. package/test/load-meter.test.ts +314 -0
  309. package/test/local-declarations.test.ts +554 -0
  310. package/test/master-to-twin.test.ts +121 -33
  311. package/test/model-basis.test.ts +56 -1
  312. package/test/{board-vocabulary.test.ts → model-vocabulary.test.ts} +25 -25
  313. package/test/move-space.test.ts +149 -0
  314. package/test/mutation-gate.test.ts +19 -5
  315. package/test/oee-accumulator.test.ts +26 -7
  316. package/test/operation-basis.test.ts +100 -0
  317. package/test/operations-capability-db.test.ts +165 -0
  318. package/test/project-structure-db.test.ts +226 -0
  319. package/test/projection-reaches-screen.test.ts +272 -0
  320. package/test/property-effects.test.ts +96 -0
  321. package/test/resync-origin-site.test.ts +77 -0
  322. package/test/scale-twin-bench.test.ts +5 -5
  323. package/test/snapshot-freshness.test.ts +60 -0
  324. package/test/space-integrity.test.ts +223 -0
  325. package/test/standard-coverage.test.ts +94 -0
  326. package/test/state-axes.test.ts +1 -1
  327. package/test/streamline-e2e.test.ts +14 -14
  328. package/test/structure-revision-db.test.ts +11 -11
  329. package/test/tenant-registry-db.test.ts +5 -5
  330. package/test/twin-audit.test.ts +83 -0
  331. package/test/twin-model-item-db.test.ts +275 -0
  332. package/test/twin-model-tree-db.test.ts +176 -0
  333. package/test/twin-origin-resync.test.ts +114 -0
  334. package/test/warm-start-seam.test.ts +6 -6
  335. package/test/warm-start.test.ts +88 -3
  336. package/tsconfig.json +6 -1
  337. package/tsconfig.shared.json +23 -0
  338. package/tsconfig.shared.tsbuildinfo +1 -0
  339. package/tsconfig.tsbuildinfo +1 -0
  340. package/dist-server/engine/board-vocabulary.js.map +0 -1
  341. package/dist-server/engine/entity-delta.js.map +0 -1
  342. package/dist-server/engine/kpi-broadcast.js.map +0 -1
  343. package/dist-server/engine/twin-level.js.map +0 -1
  344. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  345. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  346. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  347. package/dist-server/tsconfig.tsbuildinfo +0 -1
  348. package/server/service/twin-event/backfill-keys.ts +0 -72
  349. /package/{dist-server/engine → dist-shared}/kpi-broadcast.d.ts +0 -0
  350. /package/{dist-server/engine → dist-shared}/kpi-broadcast.js +0 -0
  351. /package/{dist-server/engine → dist-shared}/twin-level.d.ts +0 -0
  352. /package/{dist-server/engine → dist-shared}/twin-level.js +0 -0
  353. /package/{server/engine → shared}/kpi-broadcast.ts +0 -0
  354. /package/{server/engine → shared}/twin-level.ts +0 -0
@@ -1,10 +1,18 @@
1
+ import type { DomainSystem } from '@operato/twin-kernel';
1
2
  import type { ReferenceMaster } from './reference-master.js';
2
3
  /** 사용자 노출 파라미터(노브) 정의 — 클라이언트가 이 스키마로 폼을 렌더한다. */
3
4
  export interface KnobDef {
4
5
  key: string;
5
6
  /** 'geo' = 위경도 지점(지도에서 클릭 선택). 값은 { lat, lon }. 사용자가 좌표를 몰라도 지도로 지정. */
6
7
  label: string;
7
- type: 'number' | 'text' | 'select' | 'geo';
8
+ /**
9
+ * 'siteEquipment' = **이 현장에 이미 있는 설비들**(`{id, kind}`). 값이 비어 있으면 만들 때 한 번
10
+ * 해석해서 채우고, 채운 결과를 노브 값으로 **남긴다**(`origin.params`). 그래야 나중에 다시 지어도
11
+ * 같은 트윈이 나온다 — 그때그때 현장을 다시 읽으면 사이에 설비가 늘고 준 만큼 다른 트윈이 된다.
12
+ *
13
+ * 종류를 함께 싣는 이유: 무엇을 받아들일지는 **템플릿이 종류로 판단**한다(이름으로 짐작하지 않는다).
14
+ */
15
+ type: 'number' | 'text' | 'select' | 'geo' | 'siteEquipment';
8
16
  default: any;
9
17
  min?: number;
10
18
  max?: number;
@@ -64,11 +72,18 @@ export interface TemplateDetail {
64
72
  /** 완성 라우트 이름(공정 라우트 참조). */
65
73
  route?: string;
66
74
  };
67
- /** 표준 정합 매핑(개념 ↔ EPCIS/ISA-95). */
75
+ /**
76
+ * 표준 정합 매핑 — 개념이 **어느 표준의 무엇에 대응하나.**
77
+ *
78
+ * 표준 칸이 EPCIS·ISA-95 둘로 굳어 있었다. 에너지 템플릿은 그 둘이 앵커가 아니라 적을 자리가 없었다 —
79
+ * 커널의 `StandardClass` 도 같은 이유로 열었다(`iso50001`·`iec61850`). 대응할 이름이 없으면 **비운다**.
80
+ */
68
81
  standardsMap?: {
69
82
  concept: string;
70
83
  epcis?: string;
71
84
  isa95?: string;
85
+ iso50001?: string;
86
+ iec61850?: string;
72
87
  }[];
73
88
  /** "이 템플릿이 맞나?" 판단 도움 항목. */
74
89
  fits?: string[];
@@ -76,7 +91,8 @@ export interface TemplateDetail {
76
91
  export interface TwinTemplate {
77
92
  id: string;
78
93
  label: string;
79
- system: 'wms' | 'yms' | 'mes';
94
+ /** 커널 카탈로그가 정본 — 종류가 늘면 여기 손댈 것이 없다(`DomainSystem`). */
95
+ system: DomainSystem;
80
96
  description?: string;
81
97
  /** 상세 설명(선택) — 설명 페이지/패널이 렌더. */
82
98
  detail?: TemplateDetail;
@@ -1 +1 @@
1
- {"version":3,"file":"template-registry.js","sourceRoot":"","sources":["../../../server/service/reference/template-registry.ts"],"names":[],"mappings":";;AA6DA,4CAEC;AACD,sCAEC;AACD,kCAEC;AATD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAA;AACjD,SAAgB,gBAAgB,CAAC,GAAiB;IAChD,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAA;AAC5B,CAAC;AACD,SAAgB,aAAa;IAC3B,OAAO,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,CAAA;AAChC,CAAC;AACD,SAAgB,WAAW,CAAC,EAAU;IACpC,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC","sourcesContent":["/*\n * 트윈 템플릿(아키타입) 레지스트리 — 파라미터화된 합성 레퍼런스 마스터 빌더.\n * 레퍼런스(reference-registry)가 \"고정 마스터\"라면, 템플릿은 \"노브로 파라미터화된 마스터 빌더\".\n * 둘 다 같은 인제스트 경로(ingestMaster)로 흐른다 — 원천이 고정이냐 합성이냐만 다르다(board-authoring §10).\n * 앱(operato-twin)이 bootstrap 에서 자기 아키타입을 registerTemplate 한다. 표현(map/image/board) 무관.\n */\nimport type { ReferenceMaster } from './reference-master.js'\n\n/** 사용자 노출 파라미터(노브) 정의 — 클라이언트가 이 스키마로 폼을 렌더한다. */\nexport interface KnobDef {\n key: string\n /** 'geo' = 위경도 지점(지도에서 클릭 선택). 값은 { lat, lon }. 사용자가 좌표를 몰라도 지도로 지정. */\n label: string\n type: 'number' | 'text' | 'select' | 'geo'\n default: any\n min?: number\n max?: number\n options?: { value: string; label: string }[]\n help?: string\n}\n\n/**\n * 템플릿 상세(설명 페이지용, 선택) — \"이 템플릿이 무엇을 모델링하나\"를 구조적으로 보여줘 확신을 준다.\n * 순수 표시용 데이터(빌드 로직과 무관). 클라이언트가 이 스키마로 상세 뷰를 렌더한다.\n */\nexport interface TemplateDetail {\n /** 한 줄 요약(누구를 위한/무엇). */\n summary?: string\n standards?: string[]\n /** 밸류체인 국면(반입·보관·변환·반출 등). */\n phases?: { key: string; label: string; system?: string; note?: string }[]\n /** 공정 라우트(오퍼레이션 시퀀스). */\n route?: { op: string; label?: string; resource?: string; intent?: string; bizStep?: string }[]\n locationTypes?: { key: string; label?: string; standardClass?: string; capabilities?: string[] }[]\n resourceTypes?: { key: string; label?: string; standardClass?: string; capabilities?: string[] }[]\n /** BOM/레시피 요약. */\n recipe?: {\n inputs: { material: string; qty: number; key?: string }[]\n outputs: { material: string; qty: number; key?: string }[]\n /** 완성 라우트 이름(공정 라우트 참조). */\n route?: string\n }\n /** 표준 정합 매핑(개념 ↔ EPCIS/ISA-95). */\n standardsMap?: { concept: string; epcis?: string; isa95?: string }[]\n /** \"이 템플릿이 맞나?\" 판단 도움 항목. */\n fits?: string[]\n}\n\nexport interface TwinTemplate {\n id: string\n label: string\n system: 'wms' | 'yms' | 'mes'\n description?: string\n /** 상세 설명(선택) — 설명 페이지/패널이 렌더. */\n detail?: TemplateDetail\n knobs: KnobDef[]\n /** 노브 값 → 합성 마스터(+옵션 시나리오). scenario 는 runtime.scenario.load 로 전달되는 불투명 값. */\n build(params: Record<string, any>): { master: ReferenceMaster; scenario?: any }\n}\n\nconst templates = new Map<string, TwinTemplate>()\nexport function registerTemplate(tpl: TwinTemplate): void {\n templates.set(tpl.id, tpl)\n}\nexport function listTemplates(): TwinTemplate[] {\n return [...templates.values()]\n}\nexport function getTemplate(id: string): TwinTemplate | undefined {\n return templates.get(id)\n}\n"]}
1
+ {"version":3,"file":"template-registry.js","sourceRoot":"","sources":["../../../server/service/reference/template-registry.ts"],"names":[],"mappings":";;AA4EA,4CAEC;AACD,sCAEC;AACD,kCAEC;AATD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAA;AACjD,SAAgB,gBAAgB,CAAC,GAAiB;IAChD,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAA;AAC5B,CAAC;AACD,SAAgB,aAAa;IAC3B,OAAO,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,CAAA;AAChC,CAAC;AACD,SAAgB,WAAW,CAAC,EAAU;IACpC,OAAO,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;AAC1B,CAAC","sourcesContent":["/*\n * 트윈 템플릿(아키타입) 레지스트리 — 파라미터화된 합성 레퍼런스 마스터 빌더.\n * 레퍼런스(reference-registry)가 \"고정 마스터\"라면, 템플릿은 \"노브로 파라미터화된 마스터 빌더\".\n * 둘 다 같은 인제스트 경로(ingestMaster)로 흐른다 — 원천이 고정이냐 합성이냐만 다르다(board-authoring §10).\n * 앱(operato-twin)이 bootstrap 에서 자기 아키타입을 registerTemplate 한다. 표현(map/image/model) 무관.\n */\nimport type { DomainSystem } from '@operato/twin-kernel'\n\nimport type { ReferenceMaster } from './reference-master.js'\n\n/** 사용자 노출 파라미터(노브) 정의 — 클라이언트가 이 스키마로 폼을 렌더한다. */\nexport interface KnobDef {\n key: string\n /** 'geo' = 위경도 지점(지도에서 클릭 선택). 값은 { lat, lon }. 사용자가 좌표를 몰라도 지도로 지정. */\n label: string\n /**\n * 'siteEquipment' = **이 현장에 이미 있는 설비들**(`{id, kind}`). 값이 비어 있으면 만들 때 한 번\n * 해석해서 채우고, 채운 결과를 노브 값으로 **남긴다**(`origin.params`). 그래야 나중에 다시 지어도\n * 같은 트윈이 나온다 — 그때그때 현장을 다시 읽으면 사이에 설비가 늘고 준 만큼 다른 트윈이 된다.\n *\n * 종류를 함께 싣는 이유: 무엇을 받아들일지는 **템플릿이 종류로 판단**한다(이름으로 짐작하지 않는다).\n */\n type: 'number' | 'text' | 'select' | 'geo' | 'siteEquipment'\n default: any\n min?: number\n max?: number\n options?: { value: string; label: string }[]\n help?: string\n}\n\n/**\n * 템플릿 상세(설명 페이지용, 선택) — \"이 템플릿이 무엇을 모델링하나\"를 구조적으로 보여줘 확신을 준다.\n * 순수 표시용 데이터(빌드 로직과 무관). 클라이언트가 이 스키마로 상세 뷰를 렌더한다.\n */\nexport interface TemplateDetail {\n /** 한 줄 요약(누구를 위한/무엇). */\n summary?: string\n standards?: string[]\n /** 밸류체인 국면(반입·보관·변환·반출 등). */\n phases?: { key: string; label: string; system?: string; note?: string }[]\n /** 공정 라우트(오퍼레이션 시퀀스). */\n route?: { op: string; label?: string; resource?: string; intent?: string; bizStep?: string }[]\n locationTypes?: { key: string; label?: string; standardClass?: string; capabilities?: string[] }[]\n resourceTypes?: { key: string; label?: string; standardClass?: string; capabilities?: string[] }[]\n /** BOM/레시피 요약. */\n recipe?: {\n inputs: { material: string; qty: number; key?: string }[]\n outputs: { material: string; qty: number; key?: string }[]\n /** 완성 라우트 이름(공정 라우트 참조). */\n route?: string\n }\n /**\n * 표준 정합 매핑 — 개념이 **어느 표준의 무엇에 대응하나.**\n *\n * 표준 칸이 EPCIS·ISA-95 둘로 굳어 있었다. 에너지 템플릿은 그 둘이 앵커가 아니라 적을 자리가 없었다 —\n * 커널의 `StandardClass` 도 같은 이유로 열었다(`iso50001`·`iec61850`). 대응할 이름이 없으면 **비운다**.\n */\n standardsMap?: { concept: string; epcis?: string; isa95?: string; iso50001?: string; iec61850?: string }[]\n /** \"이 템플릿이 맞나?\" 판단 도움 항목. */\n fits?: string[]\n}\n\nexport interface TwinTemplate {\n id: string\n label: string\n /** 커널 카탈로그가 정본 — 종류가 늘면 여기 손댈 것이 없다(`DomainSystem`). */\n system: DomainSystem\n description?: string\n /** 상세 설명(선택) — 설명 페이지/패널이 렌더. */\n detail?: TemplateDetail\n knobs: KnobDef[]\n /** 노브 값 → 합성 마스터(+옵션 시나리오). scenario 는 runtime.scenario.load 로 전달되는 불투명 값. */\n build(params: Record<string, any>): { master: ReferenceMaster; scenario?: any }\n}\n\nconst templates = new Map<string, TwinTemplate>()\nexport function registerTemplate(tpl: TwinTemplate): void {\n templates.set(tpl.id, tpl)\n}\nexport function listTemplates(): TwinTemplate[] {\n return [...templates.values()]\n}\nexport function getTemplate(id: string): TwinTemplate | undefined {\n return templates.get(id)\n}\n"]}
@@ -0,0 +1,37 @@
1
+ export type CommandOrigin = 'user' | 'ai' | 'automation' | 'unknown';
2
+ export type CommandAuditInput = {
3
+ /** 클라이언트가 실어 보낸 커맨드(신뢰하지 않는 부분을 포함한 원본). */
4
+ command: any;
5
+ /** 커널 판정. */
6
+ ack: {
7
+ accepted?: boolean;
8
+ errorCode?: string;
9
+ error?: string;
10
+ };
11
+ /** 세션이 아는 사용자 — **서버만 채운다.** */
12
+ actor?: {
13
+ id?: string;
14
+ email?: string;
15
+ name?: string;
16
+ };
17
+ /** 서버 시각. 호출부가 주입해 테스트가 시간을 고정할 수 있게 한다. */
18
+ at: Date;
19
+ };
20
+ /** `TwinAuditEvent` 에 그대로 넣을 수 있는 모양(엔티티를 몰라도 되게 순수하게 둔다). */
21
+ export type CommandAuditRow = {
22
+ eventTime: Date;
23
+ commandId?: string;
24
+ commandType?: string;
25
+ origin: CommandOrigin;
26
+ reason?: string;
27
+ actorId?: string;
28
+ actorEmail?: string;
29
+ actorName?: string;
30
+ accepted: boolean;
31
+ errorCode?: string;
32
+ args?: any;
33
+ argsTruncated: boolean;
34
+ };
35
+ /** 신뢰할 수 있는 출처 표기로 정규화 — 모르는 값은 지어내지 않고 'unknown' 으로 둔다. */
36
+ export declare function normalizeOrigin(v: unknown): CommandOrigin;
37
+ export declare function buildCommandAudit(input: CommandAuditInput): CommandAuditRow;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /*
3
+ * 커맨드 감사 기록 만들기 (순수) — **누가 무엇을 허락했고, 그래서 어떻게 됐나.**
4
+ *
5
+ * 승인 게이트가 앞을 막고, 이 기록이 뒤를 설명한다. 둘 다 있어야 "사람 손을 거쳤다" 를 증명할 수 있다.
6
+ *
7
+ * 무엇을 믿는가가 이 기록의 전부다:
8
+ * · **신원·시각은 서버가 준다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 아무것도
9
+ * 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 쓴다.
10
+ * · **의도는 클라이언트가 준다** — 출처(origin)·근거(reason). 서버가 알 수 없는 정보이고,
11
+ * 위조되더라도 신원 증명을 훼손하지 않는다.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.normalizeOrigin = normalizeOrigin;
15
+ exports.buildCommandAudit = buildCommandAudit;
16
+ /** 신뢰할 수 있는 출처 표기로 정규화 — 모르는 값은 지어내지 않고 'unknown' 으로 둔다. */
17
+ function normalizeOrigin(v) {
18
+ return v === 'user' || v === 'ai' || v === 'automation' ? v : 'unknown';
19
+ }
20
+ /** args 직렬화 상한(문자). 넘으면 자르되 잘랐다고 표시한다. */
21
+ const ARGS_MAX = 4000;
22
+ function buildCommandAudit(input) {
23
+ const { command, ack, actor, at } = input;
24
+ const argsText = safeJson(command?.args);
25
+ const truncated = argsText !== null && argsText.length > ARGS_MAX;
26
+ return {
27
+ eventTime: at,
28
+ commandId: str(command?.commandId),
29
+ commandType: str(command?.type),
30
+ origin: normalizeOrigin(command?.origin),
31
+ reason: str(command?.reason),
32
+ /* 신원 — 세션 값만. 커맨드에 실려 온 신원 주장은 쳐다보지 않는다. */
33
+ actorId: str(actor?.id),
34
+ actorEmail: str(actor?.email),
35
+ actorName: str(actor?.name),
36
+ /* 결과 — 승인했는데 커널이 거절한 것도 감사 대상이다(무엇을 시도했는지가 남아야 한다). */
37
+ accepted: !!ack?.accepted,
38
+ errorCode: str(ack?.errorCode),
39
+ /* 인자는 원형 그대로. 너무 크면 잘린 문자열로 두되 잘랐다고 말한다. */
40
+ args: truncated ? argsText.slice(0, ARGS_MAX) : command?.args ?? undefined,
41
+ argsTruncated: truncated
42
+ };
43
+ }
44
+ const str = (v) => (typeof v === 'string' && v ? v : undefined);
45
+ function safeJson(v) {
46
+ try {
47
+ return v === undefined ? null : JSON.stringify(v);
48
+ }
49
+ catch {
50
+ return null; /* 순환 참조 등 — 못 재면 자르지 않는다(원본을 그대로 둔다) */
51
+ }
52
+ }
53
+ //# sourceMappingURL=command-audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-audit.js","sourceRoot":"","sources":["../../../server/service/twin-audit/command-audit.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AAgCH,0CAEC;AAKD,8CAqBC;AA7BD,4DAA4D;AAC5D,SAAgB,eAAe,CAAC,CAAU;IACxC,OAAO,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AACzE,CAAC;AAED,0CAA0C;AAC1C,MAAM,QAAQ,GAAG,IAAI,CAAA;AAErB,SAAgB,iBAAiB,CAAC,KAAwB;IACxD,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,KAAK,CAAA;IACzC,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IACxC,MAAM,SAAS,GAAG,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAA;IACjE,OAAO;QACL,SAAS,EAAE,EAAE;QACb,SAAS,EAAE,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC;QAClC,WAAW,EAAE,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC;QAC/B,MAAM,EAAE,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC;QACxC,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC;QAC5B,4CAA4C;QAC5C,OAAO,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC;QACvB,UAAU,EAAE,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC;QAC7B,SAAS,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC;QAC3B,uDAAuD;QACvD,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,QAAQ;QACzB,SAAS,EAAE,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC;QAC9B,4CAA4C;QAC5C,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,QAAS,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,IAAI,IAAI,SAAS;QAC3E,aAAa,EAAE,SAAS;KACzB,CAAA;AACH,CAAC;AAED,MAAM,GAAG,GAAG,CAAC,CAAU,EAAsB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AAE5F,SAAS,QAAQ,CAAC,CAAU;IAC1B,IAAI,CAAC;QACH,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA,CAAC,wCAAwC;IACtD,CAAC;AACH,CAAC","sourcesContent":["/*\n * 커맨드 감사 기록 만들기 (순수) — **누가 무엇을 허락했고, 그래서 어떻게 됐나.**\n *\n * 승인 게이트가 앞을 막고, 이 기록이 뒤를 설명한다. 둘 다 있어야 \"사람 손을 거쳤다\" 를 증명할 수 있다.\n *\n * 무엇을 믿는가가 이 기록의 전부다:\n * · **신원·시각은 서버가 준다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 아무것도\n * 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 쓴다.\n * · **의도는 클라이언트가 준다** — 출처(origin)·근거(reason). 서버가 알 수 없는 정보이고,\n * 위조되더라도 신원 증명을 훼손하지 않는다.\n */\n\nexport type CommandOrigin = 'user' | 'ai' | 'automation' | 'unknown'\n\nexport type CommandAuditInput = {\n /** 클라이언트가 실어 보낸 커맨드(신뢰하지 않는 부분을 포함한 원본). */\n command: any\n /** 커널 판정. */\n ack: { accepted?: boolean; errorCode?: string; error?: string }\n /** 세션이 아는 사용자 — **서버만 채운다.** */\n actor?: { id?: string; email?: string; name?: string }\n /** 서버 시각. 호출부가 주입해 테스트가 시간을 고정할 수 있게 한다. */\n at: Date\n}\n\n/** `TwinAuditEvent` 에 그대로 넣을 수 있는 모양(엔티티를 몰라도 되게 순수하게 둔다). */\nexport type CommandAuditRow = {\n eventTime: Date\n commandId?: string\n commandType?: string\n origin: CommandOrigin\n reason?: string\n actorId?: string\n actorEmail?: string\n actorName?: string\n accepted: boolean\n errorCode?: string\n args?: any\n argsTruncated: boolean\n}\n\n/** 신뢰할 수 있는 출처 표기로 정규화 — 모르는 값은 지어내지 않고 'unknown' 으로 둔다. */\nexport function normalizeOrigin(v: unknown): CommandOrigin {\n return v === 'user' || v === 'ai' || v === 'automation' ? v : 'unknown'\n}\n\n/** args 직렬화 상한(문자). 넘으면 자르되 잘랐다고 표시한다. */\nconst ARGS_MAX = 4000\n\nexport function buildCommandAudit(input: CommandAuditInput): CommandAuditRow {\n const { command, ack, actor, at } = input\n const argsText = safeJson(command?.args)\n const truncated = argsText !== null && argsText.length > ARGS_MAX\n return {\n eventTime: at,\n commandId: str(command?.commandId),\n commandType: str(command?.type),\n origin: normalizeOrigin(command?.origin),\n reason: str(command?.reason),\n /* 신원 — 세션 값만. 커맨드에 실려 온 신원 주장은 쳐다보지 않는다. */\n actorId: str(actor?.id),\n actorEmail: str(actor?.email),\n actorName: str(actor?.name),\n /* 결과 — 승인했는데 커널이 거절한 것도 감사 대상이다(무엇을 시도했는지가 남아야 한다). */\n accepted: !!ack?.accepted,\n errorCode: str(ack?.errorCode),\n /* 인자는 원형 그대로. 너무 크면 잘린 문자열로 두되 잘랐다고 말한다. */\n args: truncated ? argsText!.slice(0, ARGS_MAX) : command?.args ?? undefined,\n argsTruncated: truncated\n }\n}\n\nconst str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined)\n\nfunction safeJson(v: unknown): string | null {\n try {\n return v === undefined ? null : JSON.stringify(v)\n } catch {\n return null /* 순환 참조 등 — 못 재면 자르지 않는다(원본을 그대로 둔다) */\n }\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import { TwinAuditEvent } from './twin-audit-event.js';
2
+ import { TwinAuditQuery } from './twin-audit-query.js';
3
+ export declare const entities: (typeof TwinAuditEvent)[];
4
+ export declare const resolvers: (typeof TwinAuditQuery)[];
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvers = exports.entities = void 0;
4
+ const twin_audit_event_js_1 = require("./twin-audit-event.js");
5
+ const twin_audit_query_js_1 = require("./twin-audit-query.js");
6
+ exports.entities = [twin_audit_event_js_1.TwinAuditEvent];
7
+ exports.resolvers = [twin_audit_query_js_1.TwinAuditQuery];
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/twin-audit/index.ts"],"names":[],"mappings":";;;AAAA,+DAAsD;AACtD,+DAAsD;AAEzC,QAAA,QAAQ,GAAG,CAAC,oCAAc,CAAC,CAAA;AAC3B,QAAA,SAAS,GAAG,CAAC,oCAAc,CAAC,CAAA","sourcesContent":["import { TwinAuditEvent } from './twin-audit-event.js'\nimport { TwinAuditQuery } from './twin-audit-query.js'\n\nexport const entities = [TwinAuditEvent]\nexport const resolvers = [TwinAuditQuery]\n"]}
@@ -0,0 +1,24 @@
1
+ import { Domain } from '@things-factory/shell';
2
+ export declare class TwinAuditEvent {
3
+ readonly id: string;
4
+ domain?: Domain;
5
+ domainId?: string;
6
+ instanceId: string;
7
+ eventTime: Date;
8
+ commandId?: string;
9
+ commandType?: string;
10
+ origin?: string;
11
+ reason?: string;
12
+ actorId?: string;
13
+ actorEmail?: string;
14
+ actorName?: string;
15
+ accepted: boolean;
16
+ errorCode?: string;
17
+ /**
18
+ * 명령 인자 — **원형 그대로.** 무엇에 대한 명령이었는지가 감사의 핵심이라 요약하지 않는다.
19
+ * 지나치게 크면 자르되 `argsTruncated` 로 잘랐다는 사실을 남긴다(조용히 버리지 않는다).
20
+ */
21
+ args?: any;
22
+ argsTruncated: boolean;
23
+ createdAt?: Date;
24
+ }
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TwinAuditEvent = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const typeorm_1 = require("typeorm");
6
+ const type_graphql_1 = require("type-graphql");
7
+ const shell_1 = require("@things-factory/shell");
8
+ const env_1 = require("@things-factory/env");
9
+ /*
10
+ * 긴 글의 컬럼 타입은 **드라이버마다 이름이 다르다** — 이 레포의 정본 관행대로 여기서 고른다
11
+ * (board-service 의 `Board.model` 과 같은 방식). `text` 를 그대로 적었더니 Oracle 이 모르는 이름이었다.
12
+ */
13
+ const DATABASE_TYPE = env_1.config.get('ormconfig', {}).type;
14
+ /*
15
+ * TwinAuditEvent — **사람이 무엇을 허락했는가.**
16
+ *
17
+ * ── 왜 저널(TwinEvent)이 아닌가 ───────────────────────────────────────────────
18
+ * 처음에는 저널에 함께 넣었다. "감사는 원장에서 하니까" 라는 이유였는데, 그건 **화면의 편의**였고
19
+ * 저장 구조를 정할 근거가 못 됐다. 넣자마자 예외가 넷 생겼다 — 리플레이에서 빼고, 백필에서 빼고,
20
+ * 원장 거래 분류에서 빼고, 원장 렌더에서 갈랐다. **예외가 넷이면 설계가 "여기 아니다" 라고 말하는 것이다.**
21
+ *
22
+ * 갈라 두면 예외가 0 이고, 오히려 얻는다:
23
+ * · 행위자가 **컬럼**이 된다 — "누가 승인한 것 전부" 는 감사의 기본 질문인데 payload 안에서는 못 찾았다
24
+ * · revision 이 없다. 저널에서는 그 칸을 비워 "상태가 아님" 을 뜻하게 썼는데, 없음을 종류 표시로
25
+ * 쓰는 것이 여러 번 문제를 냈다(백필의 `bizStep IS NULL = 옛 행` 전제가 이것 때문에 깨졌다)
26
+ * · bizStep 오염이 없다 — 저널의 키 추출기는 값이 없으면 이벤트 타입으로 대체하므로,
27
+ * 감사 행을 태우면 CBV 표준 어휘 컬럼에 `governance.command` 가 박힌다
28
+ *
29
+ * ── 무엇을 믿는가 ────────────────────────────────────────────────────────────
30
+ * **신원·시각은 서버가 각인한다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 그 기록은
31
+ * 아무것도 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 신뢰한다.
32
+ * 클라이언트에서 받는 것은 **의도**뿐이다 — 어디서 시작됐나(origin), 왜 하나(reason).
33
+ */
34
+ let TwinAuditEvent = class TwinAuditEvent {
35
+ };
36
+ exports.TwinAuditEvent = TwinAuditEvent;
37
+ tslib_1.__decorate([
38
+ (0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
39
+ (0, type_graphql_1.Field)(type => type_graphql_1.ID, { description: 'Unique identifier of the audit record.' }),
40
+ tslib_1.__metadata("design:type", String)
41
+ ], TwinAuditEvent.prototype, "id", void 0);
42
+ tslib_1.__decorate([
43
+ (0, typeorm_1.ManyToOne)(type => shell_1.Domain),
44
+ (0, type_graphql_1.Field)(type => shell_1.Domain, { nullable: true, description: 'Owning tenant domain.' }),
45
+ tslib_1.__metadata("design:type", shell_1.Domain)
46
+ ], TwinAuditEvent.prototype, "domain", void 0);
47
+ tslib_1.__decorate([
48
+ (0, typeorm_1.RelationId)((e) => e.domain),
49
+ tslib_1.__metadata("design:type", String)
50
+ ], TwinAuditEvent.prototype, "domainId", void 0);
51
+ tslib_1.__decorate([
52
+ (0, typeorm_1.Column)(),
53
+ (0, type_graphql_1.Field)({ description: 'Twin runtime instance the command was aimed at.' }),
54
+ tslib_1.__metadata("design:type", String)
55
+ ], TwinAuditEvent.prototype, "instanceId", void 0);
56
+ tslib_1.__decorate([
57
+ (0, typeorm_1.Column)(),
58
+ (0, type_graphql_1.Field)({ description: 'Server time when the command was dispatched.' }),
59
+ tslib_1.__metadata("design:type", Date)
60
+ ], TwinAuditEvent.prototype, "eventTime", void 0);
61
+ tslib_1.__decorate([
62
+ (0, typeorm_1.Column)({ nullable: true }),
63
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Client-supplied command id, for correlating with kernel logs.' }),
64
+ tslib_1.__metadata("design:type", String)
65
+ ], TwinAuditEvent.prototype, "commandId", void 0);
66
+ tslib_1.__decorate([
67
+ (0, typeorm_1.Column)({ nullable: true }),
68
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Command type (e.g. order.hold, equipment.repair).' }),
69
+ tslib_1.__metadata("design:type", String)
70
+ ], TwinAuditEvent.prototype, "commandType", void 0);
71
+ tslib_1.__decorate([
72
+ (0, typeorm_1.Column)({ nullable: true }),
73
+ (0, type_graphql_1.Field)({ nullable: true, description: "Where the command originated: 'user' | 'ai' | 'automation' | 'unknown'." }),
74
+ tslib_1.__metadata("design:type", String)
75
+ ], TwinAuditEvent.prototype, "origin", void 0);
76
+ tslib_1.__decorate([
77
+ (0, typeorm_1.Column)({
78
+ nullable: true,
79
+ type: DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'
80
+ ? 'longtext'
81
+ : DATABASE_TYPE == 'oracle'
82
+ ? 'clob'
83
+ : DATABASE_TYPE == 'mssql'
84
+ ? 'nvarchar'
85
+ : 'text',
86
+ length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined
87
+ }),
88
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Why this action — the rationale shown on the approval card.' }),
89
+ tslib_1.__metadata("design:type", String)
90
+ ], TwinAuditEvent.prototype, "reason", void 0);
91
+ tslib_1.__decorate([
92
+ (0, typeorm_1.Column)({ nullable: true }),
93
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Approving user id, stamped by the server from the session.' }),
94
+ tslib_1.__metadata("design:type", String)
95
+ ], TwinAuditEvent.prototype, "actorId", void 0);
96
+ tslib_1.__decorate([
97
+ (0, typeorm_1.Column)({ nullable: true }),
98
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Approving user email, stamped by the server from the session.' }),
99
+ tslib_1.__metadata("design:type", String)
100
+ ], TwinAuditEvent.prototype, "actorEmail", void 0);
101
+ tslib_1.__decorate([
102
+ (0, typeorm_1.Column)({ nullable: true }),
103
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Approving user name, stamped by the server from the session.' }),
104
+ tslib_1.__metadata("design:type", String)
105
+ ], TwinAuditEvent.prototype, "actorName", void 0);
106
+ tslib_1.__decorate([
107
+ (0, typeorm_1.Column)({ default: false }),
108
+ (0, type_graphql_1.Field)({ description: 'Whether the kernel accepted the command.' }),
109
+ tslib_1.__metadata("design:type", Boolean)
110
+ ], TwinAuditEvent.prototype, "accepted", void 0);
111
+ tslib_1.__decorate([
112
+ (0, typeorm_1.Column)({ nullable: true }),
113
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Language-neutral rejection code when the kernel refused.' }),
114
+ tslib_1.__metadata("design:type", String)
115
+ ], TwinAuditEvent.prototype, "errorCode", void 0);
116
+ tslib_1.__decorate([
117
+ (0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
118
+ (0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'Command arguments as given.' }),
119
+ tslib_1.__metadata("design:type", Object)
120
+ ], TwinAuditEvent.prototype, "args", void 0);
121
+ tslib_1.__decorate([
122
+ (0, typeorm_1.Column)({ default: false }),
123
+ (0, type_graphql_1.Field)({ description: 'True when args were too large and stored truncated.' }),
124
+ tslib_1.__metadata("design:type", Boolean)
125
+ ], TwinAuditEvent.prototype, "argsTruncated", void 0);
126
+ tslib_1.__decorate([
127
+ (0, typeorm_1.CreateDateColumn)(),
128
+ (0, type_graphql_1.Field)({ nullable: true, description: 'Timestamp when the audit row was written.' }),
129
+ tslib_1.__metadata("design:type", Date)
130
+ ], TwinAuditEvent.prototype, "createdAt", void 0);
131
+ exports.TwinAuditEvent = TwinAuditEvent = tslib_1.__decorate([
132
+ (0, typeorm_1.Entity)()
133
+ /* 목록 기본 정렬 — 최근 것부터. 감사는 "요즘 무슨 일이 있었나" 로 시작한다. */
134
+ ,
135
+ (0, typeorm_1.Index)('ix_twin_audit_0', (e) => [e.domain, e.instanceId, e.eventTime], { unique: false })
136
+ /* 행위자로 찾기 — 저널에 있을 때는 불가능했던 질문("김현장이 승인한 것 전부"). */
137
+ ,
138
+ (0, typeorm_1.Index)('ix_twin_audit_1', (e) => [e.domain, e.actorEmail, e.eventTime], { unique: false }),
139
+ (0, type_graphql_1.ObjectType)({ description: 'Audit record of a twin command: who approved what, and what the kernel answered.' })
140
+ ], TwinAuditEvent);
141
+ //# sourceMappingURL=twin-audit-event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-audit-event.js","sourceRoot":"","sources":["../../../server/service/twin-audit/twin-audit-event.ts"],"names":[],"mappings":"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","sourcesContent":["import { CreateDateColumn, Entity, Index, Column, RelationId, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'\nimport { ObjectType, Field, ID } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\nimport { config } from '@things-factory/env'\n\n/*\n * 긴 글의 컬럼 타입은 **드라이버마다 이름이 다르다** — 이 레포의 정본 관행대로 여기서 고른다\n * (board-service 의 `Board.model` 과 같은 방식). `text` 를 그대로 적었더니 Oracle 이 모르는 이름이었다.\n */\nconst DATABASE_TYPE = config.get('ormconfig', {}).type\n\n/*\n * TwinAuditEvent — **사람이 무엇을 허락했는가.**\n *\n * ── 왜 저널(TwinEvent)이 아닌가 ───────────────────────────────────────────────\n * 처음에는 저널에 함께 넣었다. \"감사는 원장에서 하니까\" 라는 이유였는데, 그건 **화면의 편의**였고\n * 저장 구조를 정할 근거가 못 됐다. 넣자마자 예외가 넷 생겼다 — 리플레이에서 빼고, 백필에서 빼고,\n * 원장 거래 분류에서 빼고, 원장 렌더에서 갈랐다. **예외가 넷이면 설계가 \"여기 아니다\" 라고 말하는 것이다.**\n *\n * 갈라 두면 예외가 0 이고, 오히려 얻는다:\n * · 행위자가 **컬럼**이 된다 — \"누가 승인한 것 전부\" 는 감사의 기본 질문인데 payload 안에서는 못 찾았다\n * · revision 이 없다. 저널에서는 그 칸을 비워 \"상태가 아님\" 을 뜻하게 썼는데, 없음을 종류 표시로\n * 쓰는 것이 여러 번 문제를 냈다(백필의 `bizStep IS NULL = 옛 행` 전제가 이것 때문에 깨졌다)\n * · bizStep 오염이 없다 — 저널의 키 추출기는 값이 없으면 이벤트 타입으로 대체하므로,\n * 감사 행을 태우면 CBV 표준 어휘 컬럼에 `governance.command` 가 박힌다\n *\n * ── 무엇을 믿는가 ────────────────────────────────────────────────────────────\n * **신원·시각은 서버가 각인한다.** 클라이언트가 말한 신원을 그대로 적으면 위조가 자유로워 그 기록은\n * 아무것도 증명하지 못한다. 세션이 아는 사용자와 서버 시각만 신뢰한다.\n * 클라이언트에서 받는 것은 **의도**뿐이다 — 어디서 시작됐나(origin), 왜 하나(reason).\n */\n@Entity()\n/* 목록 기본 정렬 — 최근 것부터. 감사는 \"요즘 무슨 일이 있었나\" 로 시작한다. */\n@Index('ix_twin_audit_0', (e: TwinAuditEvent) => [e.domain, e.instanceId, e.eventTime], { unique: false })\n/* 행위자로 찾기 — 저널에 있을 때는 불가능했던 질문(\"김현장이 승인한 것 전부\"). */\n@Index('ix_twin_audit_1', (e: TwinAuditEvent) => [e.domain, e.actorEmail, e.eventTime], { unique: false })\n@ObjectType({ description: 'Audit record of a twin command: who approved what, and what the kernel answered.' })\nexport class TwinAuditEvent {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier of the audit record.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true, description: 'Owning tenant domain.' })\n domain?: Domain\n\n @RelationId((e: TwinAuditEvent) => e.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'Twin runtime instance the command was aimed at.' })\n instanceId: string\n\n /*\n * 서버 시각. 클라이언트 시계를 믿지 않는다 — 기록의 시간축이 단말 설정에 좌우되면 안 된다.\n *\n * **타입 이름은 적지 않는다** (2026-08-19 Postgres 부팅 실패로 고침).\n * `datetime` 은 sqlite·MySQL 의 이름이고 Postgres 는 `timestamp` 만 안다 — 거꾸로 `timestamp` 는\n * sqlite 가 모른다. **다섯 드라이버에 다 통하는 이름이 없다.** 그래서 이름 고르는 일을 TypeORM 에\n * 맡긴다(`emitDecoratorMetadata` 가 켜져 있어 `Date` 에서 드라이버별 타입을 고른다).\n */\n @Column()\n @Field({ description: 'Server time when the command was dispatched.' })\n eventTime: Date\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Client-supplied command id, for correlating with kernel logs.' })\n commandId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Command type (e.g. order.hold, equipment.repair).' })\n commandType?: string\n\n /* 'user' | 'ai' | 'automation' | 'unknown'. 모르면 unknown 이지 사람이 한 것으로 치지 않는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"Where the command originated: 'user' | 'ai' | 'automation' | 'unknown'.\" })\n origin?: string\n\n /*\n * 승인 사유 — 사람이 쓰는 글이라 길이를 미리 자르지 않는다.\n *\n * 이름을 드라이버별로 고른다(2026-08-19): `text` 는 Oracle 이 모르고(그쪽은 `clob`), MySQL 계열은\n * 긴 글에 `longtext` 를, MSSQL 은 `nvarchar(MAX)` 를 쓴다. 다중 DB 를 지키는 이 레포의 관행이 그렇다.\n */\n @Column({\n nullable: true,\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ nullable: true, description: 'Why this action — the rationale shown on the approval card.' })\n reason?: string\n\n /* ── 신원: 세션에서만 온다 ── */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Approving user id, stamped by the server from the session.' })\n actorId?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Approving user email, stamped by the server from the session.' })\n actorEmail?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Approving user name, stamped by the server from the session.' })\n actorName?: string\n\n /* ── 결과: 거절도 남긴다 ── */\n @Column({ default: false })\n @Field({ description: 'Whether the kernel accepted the command.' })\n accepted: boolean\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Language-neutral rejection code when the kernel refused.' })\n errorCode?: string\n\n /**\n * 명령 인자 — **원형 그대로.** 무엇에 대한 명령이었는지가 감사의 핵심이라 요약하지 않는다.\n * 지나치게 크면 자르되 `argsTruncated` 로 잘랐다는 사실을 남긴다(조용히 버리지 않는다).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'Command arguments as given.' })\n args?: any\n\n @Column({ default: false })\n @Field({ description: 'True when args were too large and stored truncated.' })\n argsTruncated: boolean\n\n @CreateDateColumn()\n @Field({ nullable: true, description: 'Timestamp when the audit row was written.' })\n createdAt?: Date\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import { ListParam } from '@things-factory/shell';
2
+ export declare class TwinAuditQuery {
3
+ twinAuditList(params: ListParam, context: ResolverContext): Promise<any>;
4
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TwinAuditQuery = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const shell_1 = require("@things-factory/shell");
7
+ const twin_audit_event_js_1 = require("./twin-audit-event.js");
8
+ /*
9
+ * 승인 기록 조회 — **감사의 기본 질문에 답한다.**
10
+ *
11
+ * "요즘 무슨 조치가 나갔나" → 최근순 목록
12
+ * "김현장이 승인한 것 전부" → 행위자 검색(인덱스 있음)
13
+ * "AI 가 제안한 것만" → origin 필터
14
+ * "거절된 시도" → accepted 필터
15
+ *
16
+ * 저널에 섞여 있을 때는 이 중 어느 것도 못 했다 — 행위자가 payload JSON 안이라 인덱스를 걸 수 없었다.
17
+ */
18
+ /** 시각 필터 값을 날짜로 — 못 읽는 값은 그대로 둔다(지어내지 않는다). */
19
+ function toDateValue(v) {
20
+ const one = (x) => {
21
+ if (x instanceof Date)
22
+ return x;
23
+ const t = typeof x === 'string' ? Date.parse(x) : NaN;
24
+ return Number.isNaN(t) ? x : new Date(t);
25
+ };
26
+ return Array.isArray(v) ? v.map(one) : one(v);
27
+ }
28
+ let TwinAuditQuery = class TwinAuditQuery {
29
+ /* 조회 게이트는 달지 않는다 — 이 저장소의 규약이고(mutation-gate 검사가 정본을 지킨다),
30
+ 도메인 필터가 테넌트 경계를 이미 지킨다. 테넌트를 넘어 읽는 계기판만 OPS_GATE 를 쓴다. */
31
+ async twinAuditList(params, context) {
32
+ const domainId = context.state.domain?.id;
33
+ if (!domainId)
34
+ return { items: [], total: 0 };
35
+ const effective = {
36
+ ...(params ?? {}),
37
+ /* 기본 정렬 — 최근 것부터. 감사는 "요즘" 에서 시작한다. */
38
+ sortings: params?.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],
39
+ /* 시각 필터는 날짜로 옮겨서 넘긴다 — 문자열 그대로면 조용히 0건이 된다(저널과 같은 이유). */
40
+ filters: (params?.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))
41
+ };
42
+ const qb = (0, shell_1.getQueryBuilderFromListParams)({
43
+ repository: (0, shell_1.getRepository)(twin_audit_event_js_1.TwinAuditEvent),
44
+ params: effective,
45
+ domain: context.state.domain,
46
+ alias: 'a',
47
+ /*
48
+ * `searchables` 는 전부 **실컬럼**이다 — 프레임워크 질의 빌더는 여기 없는 컬럼의 LIKE 를
49
+ * 경고 후 무시한다(인덱스 없는 전체 스캔 방지). 행위자로 찾는 것이 이 테이블을 구분한 이유다.
50
+ */
51
+ searchables: ['actorEmail', 'actorName', 'commandType', 'origin', 'errorCode', 'reason'],
52
+ sortables: ['eventTime', 'commandType', 'actorEmail', 'origin', 'accepted'],
53
+ /* 감사 목록은 사람이 읽는 속도로 본다 — 크게 퍼 오는 것이 이득이 아니다. */
54
+ defaultLimit: 50,
55
+ maxLimit: 200
56
+ });
57
+ const [items, total] = await qb.getManyAndCount();
58
+ return { items, total };
59
+ }
60
+ };
61
+ exports.TwinAuditQuery = TwinAuditQuery;
62
+ tslib_1.__decorate([
63
+ (0, type_graphql_1.Query)(returns => shell_1.ScalarObject, {
64
+ nullable: true,
65
+ description: 'List twin command audit records (who approved what, and what the kernel answered) with the standard list contract, plus the total match count.'
66
+ }),
67
+ tslib_1.__param(0, (0, type_graphql_1.Args)(type => shell_1.ListParam)),
68
+ tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
69
+ tslib_1.__metadata("design:type", Function),
70
+ tslib_1.__metadata("design:paramtypes", [shell_1.ListParam, Object]),
71
+ tslib_1.__metadata("design:returntype", Promise)
72
+ ], TwinAuditQuery.prototype, "twinAuditList", null);
73
+ exports.TwinAuditQuery = TwinAuditQuery = tslib_1.__decorate([
74
+ (0, type_graphql_1.Resolver)()
75
+ ], TwinAuditQuery);
76
+ //# sourceMappingURL=twin-audit-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-audit-query.js","sourceRoot":"","sources":["../../../server/service/twin-audit/twin-audit-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Args, Ctx, Query, Resolver } from 'type-graphql'\n\nimport { ListParam, getQueryBuilderFromListParams, getRepository, ScalarObject } from '@things-factory/shell'\n\nimport { TwinAuditEvent } from './twin-audit-event.js'\n\n/*\n * 승인 기록 조회 — **감사의 기본 질문에 답한다.**\n *\n * \"요즘 무슨 조치가 나갔나\" → 최근순 목록\n * \"김현장이 승인한 것 전부\" → 행위자 검색(인덱스 있음)\n * \"AI 가 제안한 것만\" → origin 필터\n * \"거절된 시도\" → accepted 필터\n *\n * 저널에 섞여 있을 때는 이 중 어느 것도 못 했다 — 행위자가 payload JSON 안이라 인덱스를 걸 수 없었다.\n */\n/** 시각 필터 값을 날짜로 — 못 읽는 값은 그대로 둔다(지어내지 않는다). */\nfunction toDateValue(v: any): any {\n const one = (x: any) => {\n if (x instanceof Date) return x\n const t = typeof x === 'string' ? Date.parse(x) : NaN\n return Number.isNaN(t) ? x : new Date(t)\n }\n return Array.isArray(v) ? v.map(one) : one(v)\n}\n\n@Resolver()\nexport class TwinAuditQuery {\n /* 조회 게이트는 달지 않는다 — 이 저장소의 규약이고(mutation-gate 검사가 정본을 지킨다),\n 도메인 필터가 테넌트 경계를 이미 지킨다. 테넌트를 넘어 읽는 계기판만 OPS_GATE 를 쓴다. */\n @Query(returns => ScalarObject, {\n nullable: true,\n description:\n 'List twin command audit records (who approved what, and what the kernel answered) with the standard list contract, plus the total match count.'\n })\n async twinAuditList(\n /* 표준 목록 계약은 `@Args` 로 펼쳐 받는다 — `@Arg` 로 받으면 type-graphql 이 입력 타입을\n 결정하지 못해 **스키마 조립 자체가 실패**한다(서버가 아예 안 뜬다). */\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { items: [], total: 0 }\n\n const effective: ListParam = {\n ...(params ?? ({} as ListParam)),\n /* 기본 정렬 — 최근 것부터. 감사는 \"요즘\" 에서 시작한다. */\n sortings: params?.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],\n /* 시각 필터는 날짜로 옮겨서 넘긴다 — 문자열 그대로면 조용히 0건이 된다(저널과 같은 이유). */\n filters: (params?.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinAuditEvent),\n params: effective,\n domain: context.state.domain,\n alias: 'a',\n /*\n * `searchables` 는 전부 **실컬럼**이다 — 프레임워크 질의 빌더는 여기 없는 컬럼의 LIKE 를\n * 경고 후 무시한다(인덱스 없는 전체 스캔 방지). 행위자로 찾는 것이 이 테이블을 구분한 이유다.\n */\n searchables: ['actorEmail', 'actorName', 'commandType', 'origin', 'errorCode', 'reason'],\n sortables: ['eventTime', 'commandType', 'actorEmail', 'origin', 'accepted'],\n /* 감사 목록은 사람이 읽는 속도로 본다 — 크게 퍼 오는 것이 이득이 아니다. */\n defaultLimit: 50,\n maxLimit: 200\n })\n\n const [items, total] = await qb.getManyAndCount()\n return { items, total }\n }\n}\n"]}
@@ -5,7 +5,10 @@ const tslib_1 = require("tslib");
5
5
  const type_graphql_1 = require("type-graphql");
6
6
  const twin_kernel_1 = require("@operato/twin-kernel");
7
7
  const shell_1 = require("@things-factory/shell");
8
+ const shell_2 = require("@things-factory/shell");
8
9
  const index_js_1 = require("../../engine/index.js");
10
+ const twin_audit_event_js_1 = require("../twin-audit/twin-audit-event.js");
11
+ const command_audit_js_1 = require("../twin-audit/command-audit.js");
9
12
  /*
10
13
  * 트윈 Command·Scenario 채널 — 상향 mutation.
11
14
  * Command: runtime.dispatch(행위). Scenario: runtime.scenario.load/start/pause/setSpeed(예측 자극).
@@ -25,11 +28,28 @@ let TwinControlMutation = class TwinControlMutation {
25
28
  * 시뮬에만 있어서 미러 트윈에서는 이 한 줄이 TypeError 로 터졌다(깔끔한 거절이 아니라 실패).
26
29
  * 권위(모드) × 성격(주석/액추에이션) 판정은 한 곳에만 둔다.
27
30
  */
31
+ /* 원본을 그대로 감사에 넘긴다 — 무엇이 들어왔는지가 기록의 대상이다(정규화는 감사 모듈이 한다). */
32
+ const cmdWithOrigin = command;
28
33
  const ack = await index_js_1.TwinEngine.dispatchCommand(domainId, instanceId, command);
29
- /* 라이브 구조 변이(resource.add)는 저장 board 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */
34
+ /* 라이브 구조 변이(resource.add)는 저장 model 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */
30
35
  if (ack?.accepted && command?.type === 'resource.add') {
31
36
  await index_js_1.TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => console.error('twin syncBoardEquipment fail', err));
32
37
  }
38
+ /*
39
+ * 감사 기록 — 승인 게이트가 앞을 막고, 이 줄이 뒤를 설명한다. 둘 다 있어야 "사람 손을 거쳤다" 를
40
+ * 증명할 수 있다. 커널이 거절한 것도 남긴다 — 무엇을 시도했는지가 감사의 절반이다.
41
+ *
42
+ * 기록이 실패해도 커맨드 결과는 그대로 돌려준다. 감사가 본업을 막으면 안 되지만, 실패를 삼키지도
43
+ * 않는다(로그에 남긴다) — 조용히 안 남으면 나중에 "기록이 없다" 와 "일이 없었다" 를 구분 못 한다.
44
+ */
45
+ try {
46
+ const audit = (0, command_audit_js_1.buildCommandAudit)({ command: cmdWithOrigin, ack, actor: context.state.user, at: new Date() });
47
+ const repo = (0, shell_2.getRepository)(twin_audit_event_js_1.TwinAuditEvent);
48
+ await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }));
49
+ }
50
+ catch (err) {
51
+ console.error(`[twin-control] command audit not recorded for "${instanceId}"`, err);
52
+ }
33
53
  /* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */
34
54
  return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams };
35
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"twin-control-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-control/twin-control-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Resolver, Directive} from 'type-graphql'\n\nimport { validateScenario } from '@operato/twin-kernel'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\n\n/*\n * 트윈 Command·Scenario 채널 — 상향 mutation.\n * Command: runtime.dispatch(행위). Scenario: runtime.scenario.load/start/pause/setSpeed(예측 자극).\n * (State 하향은 twin-state subscription. tick 은 워커.)\n *\n * 인가: data 구독 리졸버와 대칭으로, 대상 인스턴스가 **호출자 도메인 소속**인지 검증한다(cross-tenant 커맨드 주입 차단).\n * command 채널은 sim 상태를 변이하므로 도메인 스코프가 필수.\n */\n@Resolver()\nexport class TwinControlMutation {\n /** Command 채널 — 오더 hold/resume/release 등. 커널 dispatch 로 위임, ack{accepted,error} 반환. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'dispatch a twin command (order.hold/resume/release …). Returns {accepted, error?}.' })\n async dispatchTwinCommand(\n @Arg('instanceId') instanceId: string,\n @Arg('command', type => ScalarObject) command: any,\n @Ctx() context: ResolverContext\n ): Promise<{ accepted: boolean; error?: string; errorCode?: string; errorParams?: Record<string, string | number> }> {\n const domainId = context.state.domain?.id\n if (!domainId) return { accepted: false, error: 'no-domain', errorCode: 'no-domain' }\n /*\n * 라우팅은 엔진이 한다 — 예전에는 여기서 `inst.runtime.dispatch` 를 곧바로 불렀고, `runtime` 이\n * 시뮬에만 있어서 미러 트윈에서는 이 한 줄이 TypeError 로 터졌다(깔끔한 거절이 아니라 실패).\n * 권위(모드) × 성격(주석/액추에이션) 판정은 한 곳에만 둔다.\n */\n const ack = await TwinEngine.dispatchCommand(domainId, instanceId, command)\n /* 라이브 구조 변이(resource.add)는 저장 board 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */\n if (ack?.accepted && command?.type === 'resource.add') {\n await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => console.error('twin syncBoardEquipment fail', err))\n }\n /* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */\n return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams }\n }\n\n /** Scenario 채널 — 생성기 시나리오 제어(load/start/pause/setSpeed). */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'control twin scenario (load|start|pause|setSpeed)' })\n async controlTwinScenario(\n @Arg('instanceId') instanceId: string,\n @Arg('action') action: string,\n @Ctx() context: ResolverContext,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any\n ): Promise<boolean> {\n const domainId = context.state.domain?.id\n /* 테넌트 격리 — 레지스트리를 직접 색인하지 않는다(키에 도메인이 들어 있다). */\n if (!TwinEngine.owns(domainId, instanceId)) return false\n const runtime = TwinEngine.runtime(domainId, instanceId)\n /*\n * 자극 주입 가드 — mirror 트윈(외부 실물이 진실)은 커널 시나리오 엔진이 없다(startLive=projector, runtime 없음).\n * what-if 를 여기에 주입하면 현실을 오염시키므로 거부한다(sim-experiment/sim-world 만 자극 가능). silent throw 방지.\n */\n if (!runtime?.scenario) return false\n /*\n * **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour`\n * 를 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다).\n * 규칙은 커널이 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.\n */\n if (action === 'load') {\n const v = validateScenario(scenario)\n if (v.ok === false) {\n /* 사유를 로그로 남긴다 — 이 뮤테이션의 반환은 Boolean 계약이라 사유를 실어 보낼 자리가 없다. */\n console.warn(`[twin] scenario rejected for \"${instanceId}\": ${v.errorCode} ${JSON.stringify(v.errorParams ?? {})}`)\n return false\n }\n }\n if (action === 'load' && scenario) runtime.scenario.load(scenario)\n else if (action === 'start') runtime.scenario.start()\n else if (action === 'pause') runtime.scenario.pause()\n else if (action === 'setSpeed' && scenario?.speed != null) runtime.scenario.setSpeed(Number(scenario.speed))\n else return false\n return true\n }\n}\n"]}
1
+ {"version":3,"file":"twin-control-mutation.js","sourceRoot":"","sources":["../../../server/service/twin-control/twin-control-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Mutation, Resolver, Directive} from 'type-graphql'\n\nimport { validateScenario } from '@operato/twin-kernel'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { TwinAuditEvent } from '../twin-audit/twin-audit-event.js'\nimport { buildCommandAudit } from '../twin-audit/command-audit.js'\n\n/*\n * 트윈 Command·Scenario 채널 — 상향 mutation.\n * Command: runtime.dispatch(행위). Scenario: runtime.scenario.load/start/pause/setSpeed(예측 자극).\n * (State 하향은 twin-state subscription. tick 은 워커.)\n *\n * 인가: data 구독 리졸버와 대칭으로, 대상 인스턴스가 **호출자 도메인 소속**인지 검증한다(cross-tenant 커맨드 주입 차단).\n * command 채널은 sim 상태를 변이하므로 도메인 스코프가 필수.\n */\n@Resolver()\nexport class TwinControlMutation {\n /** Command 채널 — 오더 hold/resume/release 등. 커널 dispatch 로 위임, ack{accepted,error} 반환. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'dispatch a twin command (order.hold/resume/release …). Returns {accepted, error?}.' })\n async dispatchTwinCommand(\n @Arg('instanceId') instanceId: string,\n @Arg('command', type => ScalarObject) command: any,\n @Ctx() context: ResolverContext\n ): Promise<{ accepted: boolean; error?: string; errorCode?: string; errorParams?: Record<string, string | number> }> {\n const domainId = context.state.domain?.id\n if (!domainId) return { accepted: false, error: 'no-domain', errorCode: 'no-domain' }\n /*\n * 라우팅은 엔진이 한다 — 예전에는 여기서 `inst.runtime.dispatch` 를 곧바로 불렀고, `runtime` 이\n * 시뮬에만 있어서 미러 트윈에서는 이 한 줄이 TypeError 로 터졌다(깔끔한 거절이 아니라 실패).\n * 권위(모드) × 성격(주석/액추에이션) 판정은 한 곳에만 둔다.\n */\n /* 원본을 그대로 감사에 넘긴다 — 무엇이 들어왔는지가 기록의 대상이다(정규화는 감사 모듈이 한다). */\n const cmdWithOrigin = command\n const ack = await TwinEngine.dispatchCommand(domainId, instanceId, command)\n /* 라이브 구조 변이(resource.add)는 저장 model 에도 반영 — 프로비저닝 편집기·재기동이 추가분을 잃지 않게. */\n if (ack?.accepted && command?.type === 'resource.add') {\n await TwinEngine.syncBoardEquipment(domainId, instanceId).catch(err => console.error('twin syncBoardEquipment fail', err))\n }\n /*\n * 감사 기록 — 승인 게이트가 앞을 막고, 이 줄이 뒤를 설명한다. 둘 다 있어야 \"사람 손을 거쳤다\" 를\n * 증명할 수 있다. 커널이 거절한 것도 남긴다 — 무엇을 시도했는지가 감사의 절반이다.\n *\n * 기록이 실패해도 커맨드 결과는 그대로 돌려준다. 감사가 본업을 막으면 안 되지만, 실패를 삼키지도\n * 않는다(로그에 남긴다) — 조용히 안 남으면 나중에 \"기록이 없다\" 와 \"일이 없었다\" 를 구분 못 한다.\n */\n try {\n const audit = buildCommandAudit({ command: cmdWithOrigin, ack, actor: context.state.user, at: new Date() })\n const repo = getRepository(TwinAuditEvent)\n await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))\n } catch (err) {\n console.error(`[twin-control] command audit not recorded for \"${instanceId}\"`, err)\n }\n\n /* 거절 사유는 **언어 중립 코드**로 함께 돌려준다 — 화면이 `twin.cmderr.<code>` 로 사람 말을 만든다. */\n return { accepted: !!ack?.accepted, error: ack?.error, errorCode: ack?.errorCode, errorParams: ack?.errorParams }\n }\n\n /** Scenario 채널 — 생성기 시나리오 제어(load/start/pause/setSpeed). */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => Boolean, { description: 'control twin scenario (load|start|pause|setSpeed)' })\n async controlTwinScenario(\n @Arg('instanceId') instanceId: string,\n @Arg('action') action: string,\n @Ctx() context: ResolverContext,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any\n ): Promise<boolean> {\n const domainId = context.state.domain?.id\n /* 테넌트 격리 — 레지스트리를 직접 색인하지 않는다(키에 도메인이 들어 있다). */\n if (!TwinEngine.owns(domainId, instanceId)) return false\n const runtime = TwinEngine.runtime(domainId, instanceId)\n /*\n * 자극 주입 가드 — mirror 트윈(외부 실물이 진실)은 커널 시나리오 엔진이 없다(startLive=projector, runtime 없음).\n * what-if 를 여기에 주입하면 현실을 오염시키므로 거부한다(sim-experiment/sim-world 만 자극 가능). silent throw 방지.\n */\n if (!runtime?.scenario) return false\n /*\n * **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour`\n * 를 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다).\n * 규칙은 커널이 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.\n */\n if (action === 'load') {\n const v = validateScenario(scenario)\n if (v.ok === false) {\n /* 사유를 로그로 남긴다 — 이 뮤테이션의 반환은 Boolean 계약이라 사유를 실어 보낼 자리가 없다. */\n console.warn(`[twin] scenario rejected for \"${instanceId}\": ${v.errorCode} ${JSON.stringify(v.errorParams ?? {})}`)\n return false\n }\n }\n if (action === 'load' && scenario) runtime.scenario.load(scenario)\n else if (action === 'start') runtime.scenario.start()\n else if (action === 'pause') runtime.scenario.pause()\n else if (action === 'setSpeed' && scenario?.speed != null) runtime.scenario.setSpeed(Number(scenario.speed))\n else return false\n return true\n }\n}\n"]}
@@ -28,7 +28,7 @@ export declare function locationOf(envelope: any): string | undefined;
28
28
  * 설비·무버 — 운영 델타(equipment.status·task.status)가 대상을 가리키는 축.
29
29
  *
30
30
  * EPCIS 어휘가 아니라서 다른 축 어디에도 안 잡힌다. 이게 없으면 "이 지게차가 오늘 무엇을 했나" 를
31
- * 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아 훑는 수밖에 없다.
31
+ * 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아 전수 조회하는 수밖에 없다.
32
32
  */
33
33
  export declare function equipmentIdOf(envelope: any): string | undefined;
34
34
  /** 한 이벤트에서 승격 키 전부 — 기록 경로가 이 함수 하나만 부른다. */
@@ -76,7 +76,7 @@ function locationOf(envelope) {
76
76
  * 설비·무버 — 운영 델타(equipment.status·task.status)가 대상을 가리키는 축.
77
77
  *
78
78
  * EPCIS 어휘가 아니라서 다른 축 어디에도 안 잡힌다. 이게 없으면 "이 지게차가 오늘 무엇을 했나" 를
79
- * 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아 훑는 수밖에 없다.
79
+ * 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아 전수 조회하는 수밖에 없다.
80
80
  */
81
81
  function equipmentIdOf(envelope) {
82
82
  const d = envelope?.data ?? envelope ?? {};