@things-factory/headless-twin 10.0.14 → 10.0.17

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 (478) hide show
  1. package/dist-server/engine/attention-digest.d.ts +4 -4
  2. package/dist-server/engine/attention-digest.js +7 -7
  3. package/dist-server/engine/attention-digest.js.map +1 -1
  4. package/dist-server/engine/canonical-ingest.d.ts +11 -2
  5. package/dist-server/engine/canonical-ingest.js +137 -8
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.js +1 -1
  8. package/dist-server/engine/command-routing.js.map +1 -1
  9. package/dist-server/engine/declared-stimulus.js +1 -1
  10. package/dist-server/engine/declared-stimulus.js.map +1 -1
  11. package/dist-server/engine/energy-topology.js +1 -1
  12. package/dist-server/engine/energy-topology.js.map +1 -1
  13. package/dist-server/engine/ingest-dedupe.d.ts +38 -0
  14. package/dist-server/engine/ingest-dedupe.js +145 -0
  15. package/dist-server/engine/ingest-dedupe.js.map +1 -0
  16. package/dist-server/engine/ingest-health.d.ts +124 -9
  17. package/dist-server/engine/ingest-health.js +169 -15
  18. package/dist-server/engine/ingest-health.js.map +1 -1
  19. package/dist-server/engine/integration-probes.d.ts +3 -3
  20. package/dist-server/engine/integration-probes.js +6 -6
  21. package/dist-server/engine/integration-probes.js.map +1 -1
  22. package/dist-server/engine/kpi-baseline.js +4 -4
  23. package/dist-server/engine/kpi-baseline.js.map +1 -1
  24. package/dist-server/engine/kpi-fold.d.ts +122 -12
  25. package/dist-server/engine/kpi-fold.js +247 -18
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.d.ts +11 -11
  28. package/dist-server/engine/kpi-query.js +185 -28
  29. package/dist-server/engine/kpi-query.js.map +1 -1
  30. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  31. package/dist-server/engine/live-feed-registry.js +1 -1
  32. package/dist-server/engine/live-feed-registry.js.map +1 -1
  33. package/dist-server/engine/load-meter.d.ts +1 -1
  34. package/dist-server/engine/load-meter.js.map +1 -1
  35. package/dist-server/engine/local-declarations.d.ts +7 -7
  36. package/dist-server/engine/local-declarations.js +9 -9
  37. package/dist-server/engine/local-declarations.js.map +1 -1
  38. package/dist-server/engine/log.d.ts +1 -1
  39. package/dist-server/engine/log.js +1 -1
  40. package/dist-server/engine/log.js.map +1 -1
  41. package/dist-server/engine/measured-estimator.js +1 -1
  42. package/dist-server/engine/measured-estimator.js.map +1 -1
  43. package/dist-server/engine/measured-yield.js +1 -1
  44. package/dist-server/engine/measured-yield.js.map +1 -1
  45. package/dist-server/engine/model-basis.d.ts +1 -1
  46. package/dist-server/engine/model-basis.js +1 -1
  47. package/dist-server/engine/model-basis.js.map +1 -1
  48. package/dist-server/engine/model-gap.d.ts +3 -3
  49. package/dist-server/engine/model-gap.js +1 -1
  50. package/dist-server/engine/model-gap.js.map +1 -1
  51. package/dist-server/engine/model-vocabulary.js +1 -1
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +4 -4
  54. package/dist-server/engine/oee-accumulator.js +3 -3
  55. package/dist-server/engine/oee-accumulator.js.map +1 -1
  56. package/dist-server/engine/property-effects.js +2 -2
  57. package/dist-server/engine/property-effects.js.map +1 -1
  58. package/dist-server/engine/read-time.d.ts +22 -0
  59. package/dist-server/engine/read-time.js +81 -0
  60. package/dist-server/engine/read-time.js.map +1 -0
  61. package/dist-server/engine/restart-policy.js +1 -1
  62. package/dist-server/engine/restart-policy.js.map +1 -1
  63. package/dist-server/engine/runtime-key.js +1 -1
  64. package/dist-server/engine/runtime-key.js.map +1 -1
  65. package/dist-server/engine/spec-coverage.d.ts +2 -2
  66. package/dist-server/engine/spec-coverage.js +1 -1
  67. package/dist-server/engine/spec-coverage.js.map +1 -1
  68. package/dist-server/engine/stage-path.d.ts +4 -4
  69. package/dist-server/engine/stage-path.js +1 -1
  70. package/dist-server/engine/stage-path.js.map +1 -1
  71. package/dist-server/engine/state-axes.d.ts +1 -1
  72. package/dist-server/engine/state-axes.js +2 -2
  73. package/dist-server/engine/state-axes.js.map +1 -1
  74. package/dist-server/engine/travel-estimator.d.ts +2 -2
  75. package/dist-server/engine/travel-estimator.js +1 -1
  76. package/dist-server/engine/travel-estimator.js.map +1 -1
  77. package/dist-server/engine/twin-engine.d.ts +243 -53
  78. package/dist-server/engine/twin-engine.js +690 -191
  79. package/dist-server/engine/twin-engine.js.map +1 -1
  80. package/dist-server/engine/warm-start.d.ts +2 -2
  81. package/dist-server/engine/warm-start.js +5 -5
  82. package/dist-server/engine/warm-start.js.map +1 -1
  83. package/dist-server/index.js +1 -1
  84. package/dist-server/index.js.map +1 -1
  85. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
  86. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
  87. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
  88. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
  89. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
  90. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
  91. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
  92. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
  93. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
  94. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
  95. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
  96. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
  97. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
  98. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
  99. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
  100. package/dist-server/migrations/index.js +7 -1
  101. package/dist-server/migrations/index.js.map +1 -1
  102. package/dist-server/routes.d.ts +1 -0
  103. package/dist-server/routes.js +69 -5
  104. package/dist-server/routes.js.map +1 -1
  105. package/dist-server/service/index.d.ts +2 -2
  106. package/dist-server/service/index.js +27 -24
  107. package/dist-server/service/index.js.map +1 -1
  108. package/dist-server/service/reference/control-routing.d.ts +1 -1
  109. package/dist-server/service/reference/control-routing.js +1 -1
  110. package/dist-server/service/reference/control-routing.js.map +1 -1
  111. package/dist-server/service/reference/discovery-result.d.ts +2 -2
  112. package/dist-server/service/reference/discovery-result.js +3 -3
  113. package/dist-server/service/reference/discovery-result.js.map +1 -1
  114. package/dist-server/service/reference/hook-contract.d.ts +43 -0
  115. package/dist-server/service/reference/hook-contract.js +59 -0
  116. package/dist-server/service/reference/hook-contract.js.map +1 -0
  117. package/dist-server/service/reference/ingest-space.d.ts +1 -1
  118. package/dist-server/service/reference/ingest-space.js +4 -4
  119. package/dist-server/service/reference/ingest-space.js.map +1 -1
  120. package/dist-server/service/reference/knob-defaults.d.ts +1 -1
  121. package/dist-server/service/reference/knob-defaults.js +4 -4
  122. package/dist-server/service/reference/knob-defaults.js.map +1 -1
  123. package/dist-server/service/reference/reference-adapter.d.ts +140 -8
  124. package/dist-server/service/reference/reference-adapter.js +27 -0
  125. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  126. package/dist-server/service/reference/reference-hook.d.ts +20 -0
  127. package/dist-server/service/reference/reference-hook.js +133 -0
  128. package/dist-server/service/reference/reference-hook.js.map +1 -0
  129. package/dist-server/service/reference/reference-live.d.ts +2 -2
  130. package/dist-server/service/reference/reference-live.js +88 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +14 -14
  133. package/dist-server/service/reference/reference-master.js +34 -12
  134. package/dist-server/service/reference/reference-master.js.map +1 -1
  135. package/dist-server/service/reference/reference-resolver.d.ts +1 -1
  136. package/dist-server/service/reference/reference-resolver.js +10 -10
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  139. package/dist-server/service/reference/twin-reference.js.map +1 -1
  140. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  141. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  143. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  144. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  145. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  146. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  149. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  150. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  151. package/dist-server/service/twin-event/twin-event-keys.d.ts +22 -2
  152. package/dist-server/service/twin-event/twin-event-keys.js +37 -5
  153. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  154. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  155. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  156. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  157. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  158. package/dist-server/service/twin-event/twin-event.js +32 -4
  159. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  160. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  161. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  162. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  163. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  164. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  165. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  166. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  167. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  168. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  169. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  170. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  171. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  172. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  173. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  174. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  175. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  176. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  177. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  178. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  179. package/dist-server/service/twin-journal/journal-order.js +17 -0
  180. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  181. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  182. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  183. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  184. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  185. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  186. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  187. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  188. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  189. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  190. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  191. package/dist-server/service/twin-model/epcis-coverage.js +36 -1
  192. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  193. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  194. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  195. package/dist-server/service/twin-model/isa95-coverage.d.ts +15 -2
  196. package/dist-server/service/twin-model/isa95-coverage.js +32 -8
  197. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  198. package/dist-server/service/twin-model/item-ref.js +2 -2
  199. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  200. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  201. package/dist-server/service/twin-model/name-index.js +1 -1
  202. package/dist-server/service/twin-model/name-index.js.map +1 -1
  203. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  204. package/dist-server/service/twin-model/project-structure.js +3 -3
  205. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  206. package/dist-server/service/twin-model/twin-equipment.d.ts +1 -1
  207. package/dist-server/service/twin-model/twin-equipment.js +1 -1
  208. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  209. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  210. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  211. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  212. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  213. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  214. package/dist-server/service/twin-model/twin-model-item-query.js +14 -14
  215. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  216. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  217. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  218. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  219. package/dist-server/service/twin-model/twin-model-query.js +117 -28
  220. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  221. package/dist-server/service/twin-model/twin-model-tree-query.js +22 -0
  222. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  223. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  224. package/dist-server/service/twin-model/twin-operation.js +1 -1
  225. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  226. package/dist-server/service/twin-space/move-space.js +2 -2
  227. package/dist-server/service/twin-space/move-space.js.map +1 -1
  228. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  229. package/dist-server/service/twin-space/representation-role.js +18 -0
  230. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  231. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  232. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  233. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  234. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  235. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  236. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  237. package/dist-server/service/twin-space/twin-space-resolver.js +36 -7
  238. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  239. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  240. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  241. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  242. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  243. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  244. package/dist-server/service/twin-subject/index.d.ts +9 -0
  245. package/dist-server/service/twin-subject/index.js +16 -0
  246. package/dist-server/service/twin-subject/index.js.map +1 -0
  247. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  248. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  249. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  250. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  251. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  252. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  253. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  254. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  255. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  256. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  257. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  258. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  259. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  260. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  261. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  262. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  263. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  264. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  265. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  266. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  267. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  268. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  269. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  270. package/dist-shared/entity-delta.d.ts +1 -1
  271. package/dist-shared/entity-delta.js +3 -3
  272. package/dist-shared/entity-delta.js.map +1 -1
  273. package/dist-shared/touched-items.js +2 -2
  274. package/dist-shared/touched-items.js.map +1 -1
  275. package/dist-shared/twin-level.js +1 -1
  276. package/dist-shared/twin-level.js.map +1 -1
  277. package/package.json +3 -3
  278. package/server/engine/attention-digest.ts +9 -9
  279. package/server/engine/canonical-ingest.ts +170 -13
  280. package/server/engine/command-routing.ts +1 -1
  281. package/server/engine/declared-stimulus.ts +1 -1
  282. package/server/engine/energy-topology.ts +1 -1
  283. package/server/engine/ingest-dedupe.ts +149 -0
  284. package/server/engine/ingest-health.ts +234 -20
  285. package/server/engine/integration-probes.ts +7 -7
  286. package/server/engine/kpi-baseline.ts +4 -4
  287. package/server/engine/kpi-fold.ts +395 -27
  288. package/server/engine/kpi-query.ts +212 -46
  289. package/server/engine/live-feed-registry.ts +1 -1
  290. package/server/engine/load-meter.ts +1 -1
  291. package/server/engine/local-declarations.ts +13 -13
  292. package/server/engine/log.ts +1 -1
  293. package/server/engine/measured-estimator.ts +1 -1
  294. package/server/engine/measured-yield.ts +1 -1
  295. package/server/engine/model-basis.ts +2 -2
  296. package/server/engine/model-gap.ts +3 -3
  297. package/server/engine/model-vocabulary.ts +1 -1
  298. package/server/engine/oee-accumulator.ts +4 -4
  299. package/server/engine/property-effects.ts +2 -2
  300. package/server/engine/read-time.ts +77 -0
  301. package/server/engine/restart-policy.ts +1 -1
  302. package/server/engine/runtime-key.ts +1 -1
  303. package/server/engine/spec-coverage.ts +3 -3
  304. package/server/engine/stage-path.ts +5 -5
  305. package/server/engine/state-axes.ts +2 -2
  306. package/server/engine/travel-estimator.ts +2 -2
  307. package/server/engine/twin-engine.ts +764 -201
  308. package/server/engine/warm-start.ts +6 -6
  309. package/server/index.ts +1 -1
  310. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  311. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  312. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  313. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  314. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  315. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  316. package/server/migrations/index.ts +7 -1
  317. package/server/routes.ts +68 -5
  318. package/server/service/index.ts +4 -1
  319. package/server/service/reference/control-routing.ts +1 -1
  320. package/server/service/reference/discovery-result.ts +3 -3
  321. package/server/service/reference/hook-contract.ts +103 -0
  322. package/server/service/reference/ingest-space.ts +4 -4
  323. package/server/service/reference/knob-defaults.ts +4 -4
  324. package/server/service/reference/reference-adapter.ts +166 -9
  325. package/server/service/reference/reference-hook.ts +145 -0
  326. package/server/service/reference/reference-live.ts +92 -14
  327. package/server/service/reference/reference-master.ts +39 -17
  328. package/server/service/reference/reference-resolver.ts +10 -10
  329. package/server/service/reference/twin-reference.ts +3 -3
  330. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  331. package/server/service/twin-audit/command-audit.ts +1 -1
  332. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  333. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  334. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  335. package/server/service/twin-event/twin-event-keys.ts +37 -5
  336. package/server/service/twin-event/twin-event-type.ts +18 -1
  337. package/server/service/twin-event/twin-event.ts +32 -6
  338. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  339. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  340. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  341. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  342. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  343. package/server/service/twin-instance/twin-instance.ts +4 -4
  344. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  345. package/server/service/twin-journal/event-type-split.ts +144 -0
  346. package/server/service/twin-journal/journal-order.ts +20 -0
  347. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  348. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +57 -3
  349. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  350. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  351. package/server/service/twin-model/epcis-coverage.ts +36 -1
  352. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  353. package/server/service/twin-model/isa95-coverage.ts +45 -8
  354. package/server/service/twin-model/item-ref.ts +2 -2
  355. package/server/service/twin-model/name-index.ts +1 -1
  356. package/server/service/twin-model/project-structure.ts +4 -4
  357. package/server/service/twin-model/twin-equipment.ts +2 -2
  358. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  359. package/server/service/twin-model/twin-location.ts +1 -1
  360. package/server/service/twin-model/twin-model-item-query.ts +14 -14
  361. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  362. package/server/service/twin-model/twin-model-query.ts +117 -28
  363. package/server/service/twin-model/twin-model-tree-query.ts +25 -1
  364. package/server/service/twin-model/twin-operation.ts +2 -2
  365. package/server/service/twin-space/move-space.ts +3 -3
  366. package/server/service/twin-space/representation-role.ts +15 -0
  367. package/server/service/twin-space/space-integrity.ts +1 -1
  368. package/server/service/twin-space/twin-space-representation.ts +37 -0
  369. package/server/service/twin-space/twin-space-resolver.ts +39 -7
  370. package/server/service/twin-structure/twin-structure.ts +3 -3
  371. package/server/service/twin-subject/event-subjects.ts +155 -0
  372. package/server/service/twin-subject/index.ts +10 -0
  373. package/server/service/twin-subject/item-resolver.ts +92 -0
  374. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  375. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  376. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  377. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  378. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  379. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  380. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  381. package/shared/entity-delta.ts +3 -3
  382. package/shared/touched-items.ts +2 -2
  383. package/shared/twin-level.ts +1 -1
  384. package/test/adopt-structure-live.test.ts +3 -3
  385. package/test/aggregate-not-findone.test.ts +112 -0
  386. package/test/attention-digest.test.ts +6 -6
  387. package/test/axis-read.test.ts +2 -2
  388. package/test/boot-resume.test.ts +20 -7
  389. package/test/broadcast-period.test.ts +2 -2
  390. package/test/broadcast-queue-overflow.test.ts +244 -0
  391. package/test/canonical-ingest-vocabularies.test.ts +179 -7
  392. package/test/canonical-quantity-door.test.ts +2 -2
  393. package/test/checkpoint-refuses-empty.test.ts +2 -2
  394. package/test/connector-capability-declaration.test.ts +88 -0
  395. package/test/control-capability.test.ts +1 -1
  396. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  397. package/test/declaration-reaches-model.test.ts +5 -5
  398. package/test/declared-stimulus.test.ts +4 -4
  399. package/test/discovery-result.test.ts +2 -2
  400. package/test/entity-delta.test.ts +4 -4
  401. package/test/event-subjects.test.ts +147 -0
  402. package/test/event-time-column.test.ts +3 -3
  403. package/test/event-type-split.test.ts +155 -0
  404. package/test/fold-gap-speaks.test.ts +53 -0
  405. package/test/forecast-metrics.test.ts +1 -1
  406. package/test/forecast-tuning.test.ts +1 -1
  407. package/test/ingest-health-engine.test.ts +2 -2
  408. package/test/ingest-health-wiring.test.ts +7 -5
  409. package/test/ingest-health.test.ts +6 -6
  410. package/test/ingest-history.test.ts +2 -2
  411. package/test/ingest-idempotent.test.ts +132 -0
  412. package/test/ingest-space.test.ts +3 -3
  413. package/test/integration-probes.test.ts +3 -3
  414. package/test/integration-runner.test.ts +1 -1
  415. package/test/item-ref.test.ts +1 -1
  416. package/test/journal-order-axis.test.ts +80 -0
  417. package/test/journal-read-discipline.test.ts +16 -6
  418. package/test/journal-retention.test.ts +171 -29
  419. package/test/journal-sort-axis.test.ts +1 -1
  420. package/test/journal-write-door.test.ts +3 -3
  421. package/test/journal-write-trend.test.ts +1 -1
  422. package/test/kernel-kind-guard.test.ts +3 -3
  423. package/test/knob-defaults.test.ts +1 -1
  424. package/test/kpi-baseline-db.test.ts +2 -2
  425. package/test/kpi-fold.test.ts +185 -14
  426. package/test/kpi-query-bench.test.ts +3 -3
  427. package/test/live-cursor-wiring.test.ts +6 -6
  428. package/test/live-kernel-facts.test.ts +11 -11
  429. package/test/live-mirror-parity.test.ts +3 -3
  430. package/test/load-meter.test.ts +1 -1
  431. package/test/local-declarations.test.ts +10 -10
  432. package/test/log-stamp.test.ts +1 -1
  433. package/test/measured-yield.test.ts +2 -2
  434. package/test/merge-streams.test.ts +70 -0
  435. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  436. package/test/model-basis.test.ts +2 -2
  437. package/test/model-gap.test.ts +4 -4
  438. package/test/model-vocabulary.test.ts +2 -2
  439. package/test/move-space.test.ts +1 -1
  440. package/test/oee-accumulator.test.ts +2 -2
  441. package/test/operational-vocabulary.test.ts +2 -2
  442. package/test/project-structure-db.test.ts +2 -2
  443. package/test/projection-reaches-screen.test.ts +2 -2
  444. package/test/property-effects.test.ts +3 -3
  445. package/test/read-failure-ledger.test.ts +106 -0
  446. package/test/read-failure-visible.test.ts +7 -7
  447. package/test/read-time.test.ts +118 -0
  448. package/test/reference-grounding.test.ts +1 -1
  449. package/test/reference-hook.test.ts +86 -0
  450. package/test/registry-key-guard.test.ts +1 -1
  451. package/test/rename-twin.test.ts +81 -0
  452. package/test/restart-policy.test.ts +3 -3
  453. package/test/resync-origin-site.test.ts +3 -3
  454. package/test/revision-axis.test.ts +14 -8
  455. package/test/runtime-key.test.ts +3 -3
  456. package/test/snapshot-freshness.test.ts +4 -4
  457. package/test/source-outcome-audit.test.ts +1 -1
  458. package/test/space-integrity.test.ts +1 -1
  459. package/test/space-representation.test.ts +66 -0
  460. package/test/space-scope-includes-stopped.test.ts +55 -0
  461. package/test/spec-coverage.test.ts +1 -1
  462. package/test/standard-coverage.test.ts +43 -1
  463. package/test/status-tally.test.ts +1 -1
  464. package/test/structure-revision-db.test.ts +7 -7
  465. package/test/subject-fast-path.test.ts +148 -0
  466. package/test/subject-read-model.test.ts +175 -0
  467. package/test/touched-items.test.ts +2 -2
  468. package/test/twin-audit.test.ts +1 -1
  469. package/test/twin-event-keys.test.ts +49 -3
  470. package/test/twin-model-item-db.test.ts +5 -5
  471. package/test/twin-model-tree-db.test.ts +35 -2
  472. package/test/twin-origin-resync.test.ts +1 -1
  473. package/test/warm-start-seam.test.ts +1 -1
  474. package/test/warm-start.test.ts +6 -6
  475. package/test/withheld-door.test.ts +95 -0
  476. package/test/yield-loop.test.ts +4 -4
  477. package/tsconfig.shared.tsbuildinfo +1 -1
  478. package/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"twin-attention-query.js","sourceRoot":"","sources":["../../../server/service/twin-attention/twin-attention-query.ts"],"names":[],"mappings":";;;;AAAA,+CAA6D;AAE7D,iDAAoD;AAEpD,oDAAkD;AAClD,0EAAmE;AAEnE;;;;;;GAMG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;IAIvB,AAAN,KAAK,CAAC,cAAc,CACX,OAAwB,EACQ,UAAmB,EAStB,OAAgB,EAEL,KAAc;QAE7D,IAAI,OAAO;YAAE,OAAO,CAAC,MAAM,qBAAU,CAAC,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC,UAAU,CAAA;QACzH,IAAI,CAAC,UAAU,IAAI,CAAC,qBAAU,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC;YAAE,OAAO,EAAE,CAAA,CAAC,SAAS;QAC7F,kFAAkF;QAClF,MAAM,MAAM,GAAG,qBAAU,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;QACrE,MAAM,IAAI,GAAG,MAAM,EAAE,WAAW,EAAE,EAAE,IAAI,qBAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;QAChG,MAAM,IAAI,GAAG,IAAI,EAAE,UAAU,IAAI,EAAE,CAAA;QACnC,OAAO,KAAK,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAA,sCAAgB,EAAC,IAAI,EAAE,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA;IAC7E,CAAC;IAED;;;OAGG;IAIG,AAAN,KAAK,CAAC,mBAAmB,CAChB,OAAwB,EACK,OAAgB,EACb,UAAmB,EACX,KAAc;QAE7D,IAAI,OAAO;YAAE,OAAO,qBAAU,CAAC,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,IAAI,SAAS,CAAC,CAAA;QACtG,IAAI,CAAC,UAAU,IAAI,CAAC,qBAAU,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC;YAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,cAAc,EAAE,CAAC,EAAE,kBAAkB,EAAE,EAAE,EAAE,CAAA;QAC9I,MAAM,MAAM,GAAG,qBAAU,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;QACrE,MAAM,IAAI,GAAG,MAAM,EAAE,WAAW,EAAE,EAAE,IAAI,qBAAU,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,CAAA;QAChG,OAAO,IAAA,sCAAgB,EAAC,CAAC,IAAI,EAAE,UAAU,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,KAAK,IAAI,SAAS,CAAC,CAAA;IAC/G,CAAC;CACF,CAAA;AA/CY,gDAAkB;AAIvB;IAHL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,CAAC,oBAAY,CAAC,EAAE;QAChC,WAAW,EAAE,8KAA8K;KAC5L,CAAC;IAEC,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IASrC,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAElC,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;wDAS/C;AASK;IAHL,IAAA,oBAAK,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QAC9B,WAAW,EAAE,iJAAiJ;KAC/J,CAAC;IAEC,mBAAA,IAAA,kBAAG,GAAE,CAAA;IACL,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAClC,mBAAA,IAAA,kBAAG,EAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACrC,mBAAA,IAAA,kBAAG,EAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;;;;6DAO/C;6BA9CU,kBAAkB;IAD9B,IAAA,uBAAQ,GAAE;GACE,kBAAkB,CA+C9B","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\n\n/*\n * 주목 신호(Attention) 질의 — 현재 스냅샷의 요약된 \"봐야 할 것\".\n * 커널 computeAttentions() 파생: severity(ISA-18.2/IEC 62682) · state(OPC UA A&C) ·\n * anchor(locationId/moverId/orderId = 공간 위치) · suggestedAction(조치 제안). kind 는 도메인 소유(무방언).\n * full StateSnapshot 대신 이 경량 목록을 AI read 도구·insight UI 가 소싱(대용량 덤프 회피).\n * (실시간 스트림은 twinState subscription 이 attentions 를 포함. 이건 one-shot '질의' 표면.)\n */\n@Resolver()\nexport class TwinAttentionQuery {\n @Query(returns => [ScalarObject], {\n description: 'Active attention signals (things to look at) derived from the current twin snapshot — severity (ISA-18.2), lifecycle state (OPC UA A&C), space anchor, and suggested action.'\n })\n async twinAttentions(\n @Ctx() context: ResolverContext,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /*\n * **공간으로 물으면 그 현장의 모든 렌즈에서 모아 답한다** — 한 현실을 WMS·MES·YMS 가 함께 비추므로\n * \"이 현장에서 봐야 할 것\" 은 렌즈 하나로 답할 수 없다. 예전에는 화면이 인스턴스마다 물어 손으로\n * 합쳤고, 그러면 정렬 규칙이 화면마다 갈린다.\n *\n * 경고는 **합집합이 그대로 뜻이 있는 유일한 렌즈**다. 예측은 합칠 수 없다(P50 두 개를 더할 수 없다) —\n * 그래서 예측은 여전히 인스턴스별로 답하고 화면이 나란히 놓는다.\n */\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n /** 상위 몇 건만 — 말하지 않으면 자르지 않는다. 전체 개수는 `twinAttentionDigest` 가 함께 답한다. */\n @Arg('limit', type => Int, { nullable: true }) limit?: number\n ): Promise<any[]> {\n if (spaceId) return (await TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)).attentions\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return [] // 테넌트 격리\n // 라이브 커널 스냅샷(fresh) 우선 — snapshot()=resync() 는 캐시라 커맨드 델타 지연(recover 와 동일 원천 사용).\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n const list = snap?.attentions ?? []\n return limit && limit > 0 ? digestAttentions(list, limit).attentions : list\n }\n\n /**\n * 잘랐다면 **몇 건을 잘랐는지**와 자리 색 요약까지 — 목록만 주면 화면이 \"N건 더\" 를 말할 수 없다.\n * 자리별 최고 심각도는 **전량을 근거로** 접는다(상위 N 으로 접으면 20위 밖 신호를 가진 자리가 색을 잃는다).\n */\n @Query(returns => ScalarObject, {\n description: 'Attention signals for a space (all its lenses) or one twin, with the total count and the per-location highest severity folded from all of them.'\n })\n async twinAttentionDigest(\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('limit', type => Int, { nullable: true }) limit?: number\n ): Promise<any> {\n if (spaceId) return TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return { attentions: [], attentionTotal: 0, severityByLocation: {} }\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n return digestAttentions((snap?.attentions ?? []).map((a: any) => ({ ...a, instanceId })), limit ?? undefined)\n }\n}\n"]}
1
+ {"version":3,"file":"twin-attention-query.js","sourceRoot":"","sources":["../../../server/service/twin-attention/twin-attention-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Int, Query, Resolver } from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { TwinEngine } from '../../engine/index.js'\nimport { digestAttentions } from '../../engine/attention-digest.js'\n\n/*\n * 주목 신호(Attention) 질의 — 현재 스냅샷의 요약된 \"봐야 할 것\".\n * 커널 computeAttentions() 파생: severity(ISA-18.2/IEC 62682) · state(OPC UA A&C) ·\n * anchor(locationId/moverId/orderId = 공간 위치) · suggestedAction(조치 제안). kind 는 도메인 소유(무방언).\n * full StateSnapshot 대신 이 경량 목록을 AI read 도구·insight UI 가 소싱(대용량 덤프 회피).\n * (실시간 스트림은 twinState subscription 이 attentions 를 포함. 이건 one-shot '질의' 표면.)\n */\n@Resolver()\nexport class TwinAttentionQuery {\n @Query(returns => [ScalarObject], {\n description: 'Active attention signals (things to look at) derived from the current twin snapshot — severity (ISA-18.2), lifecycle state (OPC UA A&C), space anchor, and suggested action.'\n })\n async twinAttentions(\n @Ctx() context: ResolverContext,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n /*\n * **공간으로 물으면 그 현장의 모든 렌즈에서 모아 답한다** — 한 현실을 WMS·MES·YMS 가 함께 비추므로\n * \"이 현장에서 봐야 할 것\" 은 렌즈 하나로 답할 수 없다. 예전에는 화면이 인스턴스마다 물어 손으로\n * 합쳤고, 그러면 정렬 규칙이 화면마다 갈린다.\n *\n * 경고는 **합집합이 그대로 뜻이 있는 유일한 렌즈**다. 예측은 합칠 수 없다(P50 두 개를 더할 수 없다) —\n * 그래서 예측은 여전히 인스턴스별로 답하고 화면이 나란히 놓는다.\n */\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n /** 상위 몇 건만 — 말하지 않으면 자르지 않는다. 전체 개수는 `twinAttentionDigest` 가 함께 답한다. */\n @Arg('limit', type => Int, { nullable: true }) limit?: number\n ): Promise<any[]> {\n if (spaceId) return (await TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)).attentions\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return [] // 테넌트 격리\n // 라이브 커널 스냅샷(fresh) 우선 — snapshot()=resync() 는 캐시라 커맨드 델타 지연(recover 와 동일 원천 사용).\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n const list = snap?.attentions ?? []\n return limit && limit > 0 ? digestAttentions(list, limit).attentions : list\n }\n\n /**\n * 잘랐다면 **몇 건을 잘랐는지**와 자리 색 요약까지 — 목록만 주면 화면이 \"N건 더\" 를 말할 수 없다.\n * 자리별 최고 심각도는 **전량을 근거로** 계산한다(상위 N 으로 줄이면 20위 밖 신호를 가진 자리가 색을 잃는다).\n */\n @Query(returns => ScalarObject, {\n description: 'Attention signals for a space (all its lenses) or one twin, with the total count and the per-location highest severity folded from all of them.'\n })\n async twinAttentionDigest(\n @Ctx() context: ResolverContext,\n @Arg('spaceId', { nullable: true }) spaceId?: string,\n @Arg('instanceId', { nullable: true }) instanceId?: string,\n @Arg('limit', type => Int, { nullable: true }) limit?: number\n ): Promise<any> {\n if (spaceId) return TwinEngine.attentionsOfSpace(context.state.domain.id, spaceId, limit ?? undefined)\n if (!instanceId || !TwinEngine.owns(context.state.domain.id, instanceId)) return { attentions: [], attentionTotal: 0, severityByLocation: {} }\n const kernel = TwinEngine.kernel(context.state.domain.id, instanceId)\n const snap = kernel?.getSnapshot?.() ?? TwinEngine.snapshot(context.state.domain.id, instanceId)\n return digestAttentions((snap?.attentions ?? []).map((a: any) => ({ ...a, instanceId })), limit ?? undefined)\n }\n}\n"]}
@@ -12,7 +12,7 @@ export type CommandAuditInput = {
12
12
  * **원본(어댑터)이 준 답** — 허락과 일어남 사이에 끼어 있는 것을 같은 기록에 닫는다 (ADR-0029 §1 결정 C).
13
13
  *
14
14
  * ── 왜 이 칸이 필요한가 ──────────────────────────────────────────────────
15
- * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 조용히 실패하면 원장에는 승인만
15
+ * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 오류 없이 실패하면 원장에는 승인만
16
16
  * 남는다. 그래서 어댑터의 답을 **같은 행**에 되적는다.
17
17
  *
18
18
  * 셋을 구별한다:
@@ -1 +1 @@
1
- {"version":3,"file":"command-audit.js","sourceRoot":"","sources":["../../../server/service/twin-audit/command-audit.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AAoDH,0CAEC;AAKD,8CAuBC;AAkBD,0CAGC;AApDD,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,MAAM,EAAE,GAAG,KAAK,CAAA;IACjD,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,2CAA2C;QAC3C,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClH,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;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,MAA2C;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACxE,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAC1C,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 * **원본(어댑터)이 준 답** — 허락과 일어남 사이에 끼어 있는 것을 같은 기록에 닫는다 (ADR-0029 §1 결정 C).\n *\n * ── 왜 이 칸이 필요한가 ──────────────────────────────────────────────────\n * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 조용히 실패하면 원장에는 승인만\n * 남는다. 그래서 어댑터의 답을 **같은 행**에 되적는다.\n *\n * 셋을 구별한다:\n * · 없음(`undefined`) — **원본에 묻지 않았다**(커널이 받은 명령이다). 「모른다」와 다르다.\n * · `{ answered: false }` — 물었는데 **답을 못 받았다** → `unknown` 으로 남긴다. 성공으로 적지 않는다.\n * · `{ answered: true, ok, code? }` — 원본이 답했다(적용·거절 모두 사실이다).\n */\n source?: { answered: boolean; ok?: boolean; code?: 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 /**\n * 원본이 그래서 어떻게 했나 — `applied` · `refused` · `unknown`(물었지만 답이 없다).\n * **없으면 묻지 않은 것이다**(커널이 받은 명령). 0 이나 성공으로 채우지 않는다.\n */\n sourceOutcome?: 'applied' | 'refused' | 'unknown'\n /** 원본이 준 사유 코드 — 있으면 그대로. 우리가 지어내지 않는다. */\n sourceCode?: 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, source } = 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 ...(source ? { sourceOutcome: sourceOutcomeOf(source), ...(source.code ? { sourceCode: source.code } : {}) } : {}),\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\n/**\n * 원본의 답을 세 값으로 — **답이 없으면 `unknown`**(성공으로 적지 않는다).\n *\n * 이 함수가 있는 이유: 「물었지만 답이 없다」를 실패로도 성공으로도 적을 수 없다. 실패로 적으면 일어난 일을\n * 없던 일로 만들고, 성공으로 적으면 없던 일을 일어난 일로 만든다. 셋째 값이 사실이다.\n */\nexport function sourceOutcomeOf(source: { answered: boolean; ok?: boolean }): 'applied' | 'refused' | 'unknown' {\n if (!source.answered || typeof source.ok !== 'boolean') return 'unknown'\n return source.ok ? 'applied' : 'refused'\n}\n"]}
1
+ {"version":3,"file":"command-audit.js","sourceRoot":"","sources":["../../../server/service/twin-audit/command-audit.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AAoDH,0CAEC;AAKD,8CAuBC;AAkBD,0CAGC;AApDD,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,MAAM,EAAE,GAAG,KAAK,CAAA;IACjD,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,2CAA2C;QAC3C,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,eAAe,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClH,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;AAED;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,MAA2C;IACzE,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACxE,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAC1C,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 * **원본(어댑터)이 준 답** — 허락과 일어남 사이에 끼어 있는 것을 같은 기록에 닫는다 (ADR-0029 §1 결정 C).\n *\n * ── 왜 이 칸이 필요한가 ──────────────────────────────────────────────────\n * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 오류 없이 실패하면 원장에는 승인만\n * 남는다. 그래서 어댑터의 답을 **같은 행**에 되적는다.\n *\n * 셋을 구별한다:\n * · 없음(`undefined`) — **원본에 묻지 않았다**(커널이 받은 명령이다). 「모른다」와 다르다.\n * · `{ answered: false }` — 물었는데 **답을 못 받았다** → `unknown` 으로 남긴다. 성공으로 적지 않는다.\n * · `{ answered: true, ok, code? }` — 원본이 답했다(적용·거절 모두 사실이다).\n */\n source?: { answered: boolean; ok?: boolean; code?: 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 /**\n * 원본이 그래서 어떻게 했나 — `applied` · `refused` · `unknown`(물었지만 답이 없다).\n * **없으면 묻지 않은 것이다**(커널이 받은 명령). 0 이나 성공으로 채우지 않는다.\n */\n sourceOutcome?: 'applied' | 'refused' | 'unknown'\n /** 원본이 준 사유 코드 — 있으면 그대로. 우리가 지어내지 않는다. */\n sourceCode?: 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, source } = 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 ...(source ? { sourceOutcome: sourceOutcomeOf(source), ...(source.code ? { sourceCode: source.code } : {}) } : {}),\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\n/**\n * 원본의 답을 세 값으로 — **답이 없으면 `unknown`**(성공으로 적지 않는다).\n *\n * 이 함수가 있는 이유: 「물었지만 답이 없다」를 실패로도 성공으로도 적을 수 없다. 실패로 적으면 일어난 일을\n * 없던 일로 만들고, 성공으로 적으면 없던 일을 일어난 일로 만든다. 셋째 값이 사실이다.\n */\nexport function sourceOutcomeOf(source: { answered: boolean; ok?: boolean }): 'applied' | 'refused' | 'unknown' {\n if (!source.answered || typeof source.ok !== 'boolean') return 'unknown'\n return source.ok ? 'applied' : 'refused'\n}\n"]}
@@ -19,7 +19,7 @@ export declare class TwinAuditEvent {
19
19
  errorCode?: string;
20
20
  /**
21
21
  * 명령 인자 — **원형 그대로.** 무엇에 대한 명령이었는지가 감사의 핵심이라 요약하지 않는다.
22
- * 지나치게 크면 자르되 `argsTruncated` 로 잘랐다는 사실을 남긴다(조용히 버리지 않는다).
22
+ * 지나치게 크면 자르되 `argsTruncated` 로 잘랐다는 사실을 남긴다(알리지 않고 버리지 않는다).
23
23
  */
24
24
  args?: any;
25
25
  argsTruncated: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"twin-audit-event.js","sourceRoot":"","sources":["../../../server/service/twin-audit/twin-audit-event.ts"],"names":[],"mappings":";;;;AAAA,qCAAgH;AAChH,+CAAoD;AAEpD,iDAA4D;AAC5D,6CAA4C;AAE5C;;;GAGG;AACH,MAAM,aAAa,GAAG,YAAM,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,IAAI,CAAA;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AAOI,IAAM,cAAc,GAApB,MAAM,cAAc;CAuH1B,CAAA;AAvHY,wCAAc;AAGhB;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,WAAW,EAAE,wCAAwC,EAAE,CAAC;;0CAC1D;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC;sCACvE,cAAM;8CAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,CAAiB,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;;gDAC3B;AAIjB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;;kDACxD;AAYlB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,8CAA8C,EAAE,CAAC;sCAC5D,IAAI;iDAAA;AAIf;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;iDACtF;AAIlB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,mDAAmD,EAAE,CAAC;;mDACxE;AAKpB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yEAAyE,EAAE,CAAC;;8CACnG;AAqBf;IAbC,IAAA,gBAAM,EAAC;QACN,QAAQ,EAAE,IAAI;QACd,IAAI,EACF,aAAa,IAAI,OAAO,IAAI,aAAa,IAAI,SAAS;YACpD,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,aAAa,IAAI,QAAQ;gBACzB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa,IAAI,OAAO;oBACxB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,aAAa,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;KACrD,CAAC;IACD,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6DAA6D,EAAE,CAAC;;8CACvF;AAgBf;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2JAA2J,EAAE,CAAC;;qDAC9K;AAKtB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;;kDACvE;AAInB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,4DAA4D,EAAE,CAAC;;+CACrF;AAIhB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;kDACrF;AAInB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;;iDACrF;AAKlB;IAFC,IAAA,gBAAM,EAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,0CAA0C,EAAE,CAAC;;gDAClD;AAIjB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC;;iDACjF;AAQlB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6BAA6B,EAAE,CAAC;;4CAClF;AAIV;IAFC,IAAA,gBAAM,EAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,qDAAqD,EAAE,CAAC;;qDACxD;AAItB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;sCACxE,IAAI;iDAAA;yBAtHL,cAAc;IAN1B,IAAA,gBAAM,GAAE;IACT,mDAAmD;;IAClD,IAAA,eAAK,EAAC,iBAAiB,EAAE,CAAC,CAAiB,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC1G,oDAAoD;;IACnD,IAAA,eAAK,EAAC,iBAAiB,EAAE,CAAC,CAAiB,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACzG,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,kFAAkF,EAAE,CAAC;GACnG,cAAc,CAuH1B","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 /*\n * **원본이 그래서 어떻게 했나** — 허락과 일어남 사이에 끼어 있는 것을 같은 행에 닫는다\n * (ADR-0029 §1 결정 C, 2026-08-19).\n *\n * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 조용히 실패하면 원장에는 승인만\n * 남는다. 그래서 어댑터의 답을 되적는다.\n *\n * · 비어 있음 — **원본에 묻지 않았다**(커널이 받은 명령이다). 「모른다」와 다르다.\n * · `unknown` — 물었는데 **답을 못 받았다**. 성공으로 적지 않는다.\n * · `applied` · `refused` — 원본이 답했다(둘 다 사실이다).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"What the source did with it: 'applied' | 'refused' | 'unknown' (asked but no answer). Empty means the source was not asked — the kernel took the command.\" })\n sourceOutcome?: string\n\n /** 원본이 준 사유 코드 — 있으면 그대로 적는다(우리가 지어내지 않는다). */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Reason code the source returned, as it gave it.' })\n sourceCode?: string\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"]}
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 /*\n * **원본이 그래서 어떻게 했나** — 허락과 일어남 사이에 끼어 있는 것을 같은 행에 닫는다\n * (ADR-0029 §1 결정 C, 2026-08-19).\n *\n * 명령이 원본으로 가면 감사는 「허락했다」까지만 증명한다 — 어댑터가 오류 없이 실패하면 원장에는 승인만\n * 남는다. 그래서 어댑터의 답을 되적는다.\n *\n * · 비어 있음 — **원본에 묻지 않았다**(커널이 받은 명령이다). 「모른다」와 다르다.\n * · `unknown` — 물었는데 **답을 못 받았다**. 성공으로 적지 않는다.\n * · `applied` · `refused` — 원본이 답했다(둘 다 사실이다).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: \"What the source did with it: 'applied' | 'refused' | 'unknown' (asked but no answer). Empty means the source was not asked — the kernel took the command.\" })\n sourceOutcome?: string\n\n /** 원본이 준 사유 코드 — 있으면 그대로 적는다(우리가 지어내지 않는다). */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Reason code the source returned, as it gave it.' })\n sourceCode?: string\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"]}
@@ -36,7 +36,7 @@ let TwinAuditQuery = class TwinAuditQuery {
36
36
  ...(params ?? {}),
37
37
  /* 기본 정렬 — 최근 것부터. 감사는 "요즘" 에서 시작한다. */
38
38
  sortings: params?.sortings?.length ? params.sortings : [{ name: 'eventTime', desc: true }],
39
- /* 시각 필터는 날짜로 옮겨서 넘긴다 — 문자열 그대로면 조용히 0건이 된다(저널과 같은 이유). */
39
+ /* 시각 필터는 날짜로 옮겨서 넘긴다 — 문자열 그대로면 알리지 않고 0건이 된다(저널과 같은 이유). */
40
40
  filters: (params?.filters ?? []).map(f => (f.name === 'eventTime' ? { ...f, value: toDateValue(f.value) } : f))
41
41
  };
42
42
  const qb = (0, shell_1.getQueryBuilderFromListParams)({
@@ -1 +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"]}
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"]}
@@ -43,7 +43,7 @@ let TwinControlMutation = class TwinControlMutation {
43
43
  * 증명할 수 있다. 커널이 거절한 것도 남긴다 — 무엇을 시도했는지가 감사의 절반이다.
44
44
  *
45
45
  * 기록이 실패해도 커맨드 결과는 그대로 돌려준다. 감사가 본업을 막으면 안 되지만, 실패를 삼키지도
46
- * 않는다(로그에 남긴다) — 조용히 안 남으면 나중에 "기록이 없다" 와 "일이 없었다" 를 구분 못 한다.
46
+ * 않는다(로그에 남긴다) — 알리지 않고 안 남으면 나중에 "기록이 없다" 와 "일이 없었다" 를 구분 못 한다.
47
47
  */
48
48
  try {
49
49
  const audit = (0, command_audit_js_1.buildCommandAudit)({ command: cmdWithOrigin, ack, actor: context.state.user, at: new Date() });
@@ -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 { twinError } from '../../engine/log.js'\nimport { Arg, Ctx, Mutation, Query, 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/* 구동은 원본을 지난다(ADR-0029 §8) — 트윈 런타임을 직접 잡지 않는다. */\nimport { routeControl, controlCapabilityOf } from '../reference/control-routing.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 => twinError('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 twinError(`[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 /**\n * 구동 채널 — **원본을 지난다**(ADR-0029 §8).\n *\n * ── 무엇이 바뀌었나 (2026-08-19) ──────────────────────────────────────────\n * 예전에는 이 문이 `TwinEngine.runtime(...).scenario` 를 **직접** 잡았다. 시뮬만 할 수 있는 일(자극·배속·\n * 정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. 이제 길은 하나다:\n * 트윈 → 그 트윈의 원본 → 어댑터가 **선언한** `control` 능력. 선언하지 않은 원본에는 문이 열리지 않고,\n * 그 사실을 사유 코드로 말한다(`not-declared`) — 실 WMS 가 정지되지 않는 것은 결손이 아니다.\n *\n * 반환을 Boolean 에서 객체로 바꿨다: 예전에는 거절 사유를 실을 자리가 없어 **로그로만** 남겼다.\n * 화면은 왜 안 되는지 알 수 없었다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n \"Drive the source behind a twin: stimulus | start | pause | speed | reset. The command goes through the source adapter's declared control capability — a source that does not declare it refuses with code 'not-declared' (a real system is not paused or seeded, and that is a fact rather than a gap). Returns { ok, code?, detail? }.\"\n })\n async controlTwinSource(\n @Arg('instanceId') instanceId: string,\n @Arg('action') action: string,\n @Ctx() context: ResolverContext,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any,\n @Arg('speed', { nullable: true }) speed?: number\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { ok: false, code: 'no-domain' }\n const allowed = ['stimulus', 'start', 'pause', 'speed', 'reset']\n if (!allowed.includes(action)) return { ok: false, code: 'unsupported-action', detail: `one of ${allowed.join(' | ')}` }\n /*\n * **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour` 를\n * 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다). 규칙은 커널이\n * 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.\n */\n if (action === 'stimulus') {\n const v = validateScenario(scenario)\n if (v.ok === false) return { ok: false, code: 'invalid', detail: v.errorCode, params: v.errorParams }\n }\n if (action === 'speed' && !(Number(speed) > 0)) return { ok: false, code: 'invalid', detail: 'speed must be greater than 0' }\n\n /*\n * **구동도 사람이 한 일이다 — 남긴다** (ADR-0029 §1 결정 C, 2026-08-19).\n *\n * 이 문은 감사를 아예 남기지 않았다. 그런데 자극을 바꾸고 배속을 올리고 원본을 멈추는 것은 그 트윈의\n * 사실을 바꾸는 행위다 — 커맨드 채널은 남기는데 이쪽이 비어 있으면, 나중에 「무엇이 이 공장을\n * 흔들었나」에 답할 수 없다.\n *\n * 결정 C 그대로: 게이트는 여기 남고(위 `@privilege`), **원본의 답을 같은 행에 닫는다.** 답을 못 받으면\n * `unknown` 이다 — 성공으로 적지 않는다.\n */\n let result: { ok: boolean; code?: string; detail?: string }\n try {\n result = await routeControl(domainId, instanceId, { action: action as any, scenario, speed: speed != null ? Number(speed) : undefined })\n } catch (e: any) {\n result = { ok: false, code: 'failed', detail: e?.message ?? 'control failed' }\n }\n try {\n const audit = buildCommandAudit({\n /* 커맨드 채널과 같은 모양으로 적는다 — 감사를 읽는 쪽이 두 어휘를 배우지 않게. */\n command: { type: `source.${action}`, origin: 'user', args: { ...(scenario ? { scenario } : {}), ...(speed != null ? { speed } : {}) } },\n /* 게이트는 통과했다(여기까지 왔다) — 그 사실과 원본의 답은 다른 칸이다. */\n ack: { accepted: true },\n source: { answered: typeof result?.ok === 'boolean', ok: result?.ok, code: result?.code },\n actor: context.state.user,\n at: new Date()\n })\n const repo = getRepository(TwinAuditEvent)\n await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))\n } catch (err) {\n /* 기록이 실패해도 결과는 돌려준다 — 다만 삼키지 않는다(「기록 없음」과 「일 없음」을 구별해야 한다). */\n twinError(`[twin-control] source control audit not recorded for \"${instanceId}\"`, err)\n }\n return result\n }\n\n /** 이 트윈의 원본이 **무엇을 받을 수 있나** — 화면이 버튼을 열기 전에 묻는 자리. */\n @Query(returns => ScalarObject, {\n description: \"What the source behind this twin can do: its declared capabilities ('live', 'control'). Empty when the twin has no source or its adapter declares nothing.\"\n })\n async twinSourceCapability(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { capabilities: [] }\n return controlCapabilityOf(domainId, instanceId)\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 { twinError } from '../../engine/log.js'\nimport { Arg, Ctx, Mutation, Query, 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/* 구동은 원본을 지난다(ADR-0029 §8) — 트윈 런타임을 직접 잡지 않는다. */\nimport { routeControl, controlCapabilityOf } from '../reference/control-routing.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 => twinError('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 twinError(`[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 /**\n * 구동 채널 — **원본을 지난다**(ADR-0029 §8).\n *\n * ── 무엇이 바뀌었나 (2026-08-19) ──────────────────────────────────────────\n * 예전에는 이 문이 `TwinEngine.runtime(...).scenario` 를 **직접** 잡았다. 시뮬만 할 수 있는 일(자극·배속·\n * 정지)이 계약 밖의 지름길로 들어온 셈이고, 실 시스템에는 그 자리가 아예 없었다. 이제 길은 하나다:\n * 트윈 → 그 트윈의 원본 → 어댑터가 **선언한** `control` 능력. 선언하지 않은 원본에는 문이 열리지 않고,\n * 그 사실을 사유 코드로 말한다(`not-declared`) — 실 WMS 가 정지되지 않는 것은 결손이 아니다.\n *\n * 반환을 Boolean 에서 객체로 바꿨다: 예전에는 거절 사유를 실을 자리가 없어 **로그로만** 남겼다.\n * 화면은 왜 안 되는지 알 수 없었다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n \"Drive the source behind a twin: stimulus | start | pause | speed | reset. The command goes through the source adapter's declared control capability — a source that does not declare it refuses with code 'not-declared' (a real system is not paused or seeded, and that is a fact rather than a gap). Returns { ok, code?, detail? }.\"\n })\n async controlTwinSource(\n @Arg('instanceId') instanceId: string,\n @Arg('action') action: string,\n @Ctx() context: ResolverContext,\n @Arg('scenario', type => ScalarObject, { nullable: true }) scenario?: any,\n @Arg('speed', { nullable: true }) speed?: number\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { ok: false, code: 'no-domain' }\n const allowed = ['stimulus', 'start', 'pause', 'speed', 'reset']\n if (!allowed.includes(action)) return { ok: false, code: 'unsupported-action', detail: `one of ${allowed.join(' | ')}` }\n /*\n * **잘못된 선언은 문 앞에서 막는다.** 예전에는 검증 없이 실었고, 다음 tick 에서 `rate.meanPerHour` 를\n * 읽다 터져 **서버 프로세스가 내려갔다**(클라이언트가 보낸 값 하나로 앱 전체가 죽었다). 규칙은 커널이\n * 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.\n */\n if (action === 'stimulus') {\n const v = validateScenario(scenario)\n if (v.ok === false) return { ok: false, code: 'invalid', detail: v.errorCode, params: v.errorParams }\n }\n if (action === 'speed' && !(Number(speed) > 0)) return { ok: false, code: 'invalid', detail: 'speed must be greater than 0' }\n\n /*\n * **구동도 사람이 한 일이다 — 남긴다** (ADR-0029 §1 결정 C, 2026-08-19).\n *\n * 이 문은 감사를 아예 남기지 않았다. 그런데 자극을 바꾸고 배속을 올리고 원본을 멈추는 것은 그 트윈의\n * 사실을 바꾸는 행위다 — 커맨드 채널은 남기는데 이쪽이 비어 있으면, 나중에 「무엇이 이 공장을\n * 흔들었나」에 답할 수 없다.\n *\n * 결정 C 그대로: 게이트는 여기 남고(위 `@privilege`), **원본의 답을 같은 행에 닫는다.** 답을 못 받으면\n * `unknown` 이다 — 성공으로 적지 않는다.\n */\n let result: { ok: boolean; code?: string; detail?: string }\n try {\n result = await routeControl(domainId, instanceId, { action: action as any, scenario, speed: speed != null ? Number(speed) : undefined })\n } catch (e: any) {\n result = { ok: false, code: 'failed', detail: e?.message ?? 'control failed' }\n }\n try {\n const audit = buildCommandAudit({\n /* 커맨드 채널과 같은 모양으로 적는다 — 감사를 읽는 쪽이 두 어휘를 배우지 않게. */\n command: { type: `source.${action}`, origin: 'user', args: { ...(scenario ? { scenario } : {}), ...(speed != null ? { speed } : {}) } },\n /* 게이트는 통과했다(여기까지 왔다) — 그 사실과 원본의 답은 다른 칸이다. */\n ack: { accepted: true },\n source: { answered: typeof result?.ok === 'boolean', ok: result?.ok, code: result?.code },\n actor: context.state.user,\n at: new Date()\n })\n const repo = getRepository(TwinAuditEvent)\n await repo.save(repo.create({ domain: context.state.domain, instanceId, ...audit }))\n } catch (err) {\n /* 기록이 실패해도 결과는 돌려준다 — 다만 삼키지 않는다(「기록 없음」과 「일 없음」을 구별해야 한다). */\n twinError(`[twin-control] source control audit not recorded for \"${instanceId}\"`, err)\n }\n return result\n }\n\n /** 이 트윈의 원본이 **무엇을 받을 수 있나** — 화면이 버튼을 열기 전에 묻는 자리. */\n @Query(returns => ScalarObject, {\n description: \"What the source behind this twin can do: its declared capabilities ('live', 'control'). Empty when the twin has no source or its adapter declares nothing.\"\n })\n async twinSourceCapability(@Arg('instanceId') instanceId: string, @Ctx() context: ResolverContext): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { capabilities: [] }\n return controlCapabilityOf(domainId, instanceId)\n }\n}\n"]}
@@ -17,7 +17,27 @@ export declare function bizStepOf(envelope: any): string | undefined;
17
17
  * URN 으로 찾는 경로가 사라진다.
18
18
  */
19
19
  export declare function epcOf(envelope: any): string | undefined;
20
- /** 거래 식별자(PO/SO) — EPCIS bizTransactionList 우선, 운영 델타는 `order`. */
20
+ /**
21
+ * 오더 식별자 — 운영 델타의 `orderId` 우선, EPCIS 는 `bizTransactionList`.
22
+ *
23
+ * ── 실측 (2026-08-24) — 이 컬럼이 **전부 비어 있었다** ──────────────────────
24
+ * 여기가 찾던 이름이 `d.order` 였다. 그런데 커널이 내는 이름은 **`orderId`** 다
25
+ * (`OrderStatusDelta.orderId` · `TaskStatusDelta.orderId`). `d.order` 를 내는 코드는 커널에 **한 곳도
26
+ * 없다** — 죽은 가지였다. 그래서 운영 델타는 이 컬럼을 한 번도 채우지 못했다.
27
+ *
28
+ * order.status 29,403,565 행 — order_id 비어 있음 29,403,565 (100%)
29
+ * task.status 191,175 행 — 비어 있음 191,175 (100%)
30
+ *
31
+ * 그 결과 색인 `ix_twin_event_4` 가 **자기 주석이 적어 둔 용도**(「이 오더가 어디까지 갔나」)로 쓸 수
32
+ * 없었다. 화면은 오더를 눌러도 「연결된 이벤트가 없습니다」를 냈고, 그 답은 질의 결과로는 정직했다 —
33
+ * 시점을 어디로 옮겨도 0 이었다.
34
+ *
35
+ * 순서가 중요하다: 운영 델타가 먼저다. EPCIS 의 `bizTransactionList` 는 **거래**(PO/SO)이고 오더와
36
+ * 같은 것이 아닐 수 있으므로, 오더를 스스로 말하는 사건은 그 말을 그대로 쓴다.
37
+ *
38
+ * **과거 행은 채워지지 않는다**(사용자 결정 2026-08-24, `moverId` 때와 같은 방식). 앞으로 들어오는
39
+ * 사건부터 조회된다.
40
+ */
21
41
  export declare function orderOf(envelope: any): string | undefined;
22
42
  /**
23
43
  * 위치 — EPCIS 는 읽은 지점(readPoint) 우선, 없으면 업무 위치(bizLocation).
@@ -36,7 +56,7 @@ export declare function equipmentIdOf(envelope: any): string | undefined;
36
56
  * EPCIS 행위 — `ADD`·`OBSERVE`·`DELETE`. 세 값이 아니면 `undefined`(원천의 잡값을 색인에 넣지 않는다).
37
57
  *
38
58
  * 이 키는 검색 축이 아니라 **판별식**이다: 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다.
39
- * 그래서 「이 트윈이 반출을 한 번이라도 접었는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
59
+ * 그래서 「이 트윈이 반출을 한 번이라도 계산했는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
40
60
  * 동안에는 5개 드라이버 어디에서도 그 질문을 할 수 없었다. 운영 델타에는 행위가 없다 — 비운다.
41
61
  */
42
62
  export declare function actionOf(envelope: any): string | undefined;
@@ -10,7 +10,7 @@ exports.twinEventKeys = twinEventKeys;
10
10
  const log_js_1 = require("../../engine/log.js");
11
11
  /*
12
12
  * 컬럼 길이 상한. GS1 식별자(EPC URN·GDTI·SGLN)는 규격상 이보다 훨씬 짧다.
13
- * 넘치는 값이 오면 **조용히 자르지 않고** 경고를 남긴다 — 색인이 원본과 다르면 검색 결과가 거짓이 되는데,
13
+ * 넘치는 값이 오면 **알리지 않고 자르지 않고** 경고를 남긴다 — 색인이 원본과 다르면 검색 결과가 거짓이 되는데,
14
14
  * 그 사실이 어디에도 안 남으면 아무도 모른다.
15
15
  */
16
16
  const MAX_KEY = 255;
@@ -43,10 +43,30 @@ function epcOf(envelope) {
43
43
  const d = envelope?.data ?? envelope ?? {};
44
44
  return d.epcList?.[0] ?? d.parentID ?? d.quantityList?.[0]?.epcClass ?? undefined;
45
45
  }
46
- /** 거래 식별자(PO/SO) — EPCIS bizTransactionList 우선, 운영 델타는 `order`. */
46
+ /**
47
+ * 오더 식별자 — 운영 델타의 `orderId` 우선, EPCIS 는 `bizTransactionList`.
48
+ *
49
+ * ── 실측 (2026-08-24) — 이 컬럼이 **전부 비어 있었다** ──────────────────────
50
+ * 여기가 찾던 이름이 `d.order` 였다. 그런데 커널이 내는 이름은 **`orderId`** 다
51
+ * (`OrderStatusDelta.orderId` · `TaskStatusDelta.orderId`). `d.order` 를 내는 코드는 커널에 **한 곳도
52
+ * 없다** — 죽은 가지였다. 그래서 운영 델타는 이 컬럼을 한 번도 채우지 못했다.
53
+ *
54
+ * order.status 29,403,565 행 — order_id 비어 있음 29,403,565 (100%)
55
+ * task.status 191,175 행 — 비어 있음 191,175 (100%)
56
+ *
57
+ * 그 결과 색인 `ix_twin_event_4` 가 **자기 주석이 적어 둔 용도**(「이 오더가 어디까지 갔나」)로 쓸 수
58
+ * 없었다. 화면은 오더를 눌러도 「연결된 이벤트가 없습니다」를 냈고, 그 답은 질의 결과로는 정직했다 —
59
+ * 시점을 어디로 옮겨도 0 이었다.
60
+ *
61
+ * 순서가 중요하다: 운영 델타가 먼저다. EPCIS 의 `bizTransactionList` 는 **거래**(PO/SO)이고 오더와
62
+ * 같은 것이 아닐 수 있으므로, 오더를 스스로 말하는 사건은 그 말을 그대로 쓴다.
63
+ *
64
+ * **과거 행은 채워지지 않는다**(사용자 결정 2026-08-24, `moverId` 때와 같은 방식). 앞으로 들어오는
65
+ * 사건부터 조회된다.
66
+ */
47
67
  function orderOf(envelope) {
48
68
  const d = envelope?.data ?? envelope ?? {};
49
- return d.bizTransactionList?.[0]?.bizTransaction ?? d.order ?? undefined;
69
+ return d.orderId ?? d.bizTransactionList?.[0]?.bizTransaction ?? undefined;
50
70
  }
51
71
  /**
52
72
  * 위치 — EPCIS 는 읽은 지점(readPoint) 우선, 없으면 업무 위치(bizLocation).
@@ -65,13 +85,25 @@ function locationOf(envelope) {
65
85
  */
66
86
  function equipmentIdOf(envelope) {
67
87
  const d = envelope?.data ?? envelope ?? {};
68
- return d.moverId ?? undefined;
88
+ /*
89
+ * ── 실측 (2026-08-24) — 두 갈래를 놓치고 있었다 ────────────────────────────
90
+ * 위 주석이 `task.status` 를 출처로 **적어 두었는데** 이 함수는 `d.moverId` 만 봤다. 작업이 자원을
91
+ * 가리키는 이름은 `resourceRef` 다(`TaskStatusDelta.resourceRef`) — 그래서 작업 191,175 행 전부
92
+ * 이 컬럼이 비었고, 「이 지게차가 오늘 무엇을 했나」에서 **작업이 통째로 빠졌다.**
93
+ *
94
+ * 그리고 에너지 사건은 `equipmentId` 를 쓴다(어휘가 `movers`→`equipment` 로 개명된 뒤에 생긴
95
+ * 채널이다). 실측 `energy.equipment` 31,079 행 전부 비어 있었다 — 「이 설비가 얼마를 먹었나」를
96
+ * 설비 축으로 물을 수 없었다.
97
+ *
98
+ * 셋을 함께 본다. 개명 세대가 섞여 있는 것은 저널의 성질이고, 읽는 쪽이 그것을 흡수한다.
99
+ */
100
+ return d.moverId ?? d.resourceRef ?? d.equipmentId ?? undefined;
69
101
  }
70
102
  /**
71
103
  * EPCIS 행위 — `ADD`·`OBSERVE`·`DELETE`. 세 값이 아니면 `undefined`(원천의 잡값을 색인에 넣지 않는다).
72
104
  *
73
105
  * 이 키는 검색 축이 아니라 **판별식**이다: 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다.
74
- * 그래서 「이 트윈이 반출을 한 번이라도 접었는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
106
+ * 그래서 「이 트윈이 반출을 한 번이라도 계산했는가」가 진짜 delta 피드의 증거인데, payload 안에 있는
75
107
  * 동안에는 5개 드라이버 어디에서도 그 질문을 할 수 없었다. 운영 델타에는 행위가 없다 — 비운다.
76
108
  */
77
109
  function actionOf(envelope) {
@@ -1 +1 @@
1
- {"version":3,"file":"twin-event-keys.js","sourceRoot":"","sources":["../../../server/service/twin-event/twin-event-keys.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinWarn } from '../../engine/log.js'\n/*\n * 저널 검색 키 추출 — **순수**. 인제스트가 기록할 때 한 번 뽑아 인덱스 가능한 실컬럼으로 승격한다.\n *\n * ── 왜 승격하는가 ───────────────────────────────────────────────────────────\n * 사용자가 저널에서 실제로 찾는 것은 \"이 팔레트의 이력\", \"이 오더가 어디까지 갔나\", \"이 도크에서\n * 무슨 일이 있었나\" 다. 그런데 그 값들은 전부 `payload`(simple-json = TEXT) **안**에 있었다.\n * things-factory 는 5개 DB 드라이버를 지원해야 해서 DB별 JSON 연산자를 쓸 수 없다 —\n * 즉 승격 없이는 **어떤 방법으로도 서버에서 그 조건으로 거를 수 없었다**. 클라이언트가 받아온\n * 몇 천 건 안에서만 찾는 시늉이 최선이었고, 저널이 커질수록 그 시늉은 거짓말에 가까워진다.\n *\n * 그래서 검색 축이 되는 값만 골라 컬럼으로 꺼낸다. payload 는 그대로 둔다(정본은 여전히 payload —\n * 이건 파생 색인이지 새로운 진실이 아니다).\n *\n * ── 왜 여기(순수 모듈)인가 ──────────────────────────────────────────────────\n * 기록 경로가 둘이다(`persistBatch` 라이브 벌크 · `persist` 심 단건). 두 곳에 각자 적으면\n * 반드시 어긋나고, 어긋난 색인은 \"없는 것처럼 보이는 이벤트\" 를 만든다 — 저널에서 가장 나쁜 결함이다.\n */\n\n/** 승격된 검색 키 — 전부 선택적. 뽑히지 않으면 **빈 문자열이 아니라 undefined**(결측≠빈값). */\nexport interface TwinEventKeys {\n action?: string\n bizStep?: string\n epc?: string\n orderId?: string\n locationId?: string\n moverId?: string\n}\n\n/*\n * 컬럼 길이 상한. GS1 식별자(EPC URN·GDTI·SGLN)는 규격상 이보다 훨씬 짧다.\n * 넘치는 값이 오면 **조용히 자르지 않고** 경고를 남긴다 — 색인이 원본과 다르면 검색 결과가 거짓이 되는데,\n * 그 사실이 어디에도 안 남으면 아무도 모른다.\n */\nconst MAX_KEY = 255\n\nfunction clip(v: unknown, field: string): string | undefined {\n if (v === undefined || v === null) return undefined\n const s = String(v)\n if (!s) return undefined\n if (s.length <= MAX_KEY) return s\n twinWarn(\n `[twin-event-keys] ${field} exceeds ${MAX_KEY} chars and was clipped for indexing — ` +\n `search on this value may be incomplete. payload keeps the full value. (${s.slice(0, 60)}…)`\n )\n return s.slice(0, MAX_KEY)\n}\n\n/** CBV bizStep URN 의 끝마디. 없으면 이벤트 타입에서 유추(`epcis.` 접두 제거). */\nexport function bizStepOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n const tail = String(d.bizStep ?? '').split(':').pop()\n return tail || String(envelope?.eventType ?? '').replace('epcis.', '') || undefined\n}\n\n/**\n * 품목 식별자 — **전체 값**을 저장한다(끝마디만 저장하지 않는다).\n *\n * 표시용 축약은 화면이 하고, 색인은 원본을 갖는다. `search` 는 부분일치(contains)라\n * 전체를 저장해 두면 끝마디(\"402.2\")로도 URN 전체로도 찾힌다. 반대로 끝마디만 저장하면\n * URN 으로 찾는 경로가 사라진다.\n */\nexport function epcOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n return d.epcList?.[0] ?? d.parentID ?? d.quantityList?.[0]?.epcClass ?? undefined\n}\n\n/** 거래 식별자(PO/SO) — EPCIS bizTransactionList 우선, 운영 델타는 `order`. */\nexport function orderOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n return d.bizTransactionList?.[0]?.bizTransaction ?? d.order ?? undefined\n}\n\n/**\n * 위치 — EPCIS 는 읽은 지점(readPoint) 우선, 없으면 업무 위치(bizLocation).\n * 운영 델타(무버 이동 등)는 그 둘이 없고 평범한 `location` 을 쓴다 — 빠뜨리면 설비가 어디서\n * 무엇을 했는지가 위치 축에서 통째로 사라진다.\n */\nexport function locationOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n return d.readPoint?.id ?? d.bizLocation?.id ?? d.location ?? undefined\n}\n\n/**\n * 설비·무버 — 운영 델타(equipment.status·task.status)가 대상을 가리키는 축.\n *\n * EPCIS 어휘가 아니라서 다른 축 어디에도 안 잡힌다. 이게 없으면 \"이 지게차가 오늘 무엇을 했나\" 를\n * 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아 전수 조회하는 수밖에 없다.\n */\nexport function equipmentIdOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n return d.moverId ?? undefined\n}\n\n/**\n * EPCIS 행위 — `ADD`·`OBSERVE`·`DELETE`. 세 값이 아니면 `undefined`(원천의 잡값을 색인에 넣지 않는다).\n *\n * 이 키는 검색 축이 아니라 **판별식**이다: 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다.\n * 그래서 「이 트윈이 반출을 한 번이라도 접었는가」가 진짜 delta 피드의 증거인데, payload 안에 있는\n * 동안에는 5개 드라이버 어디에서도 그 질문을 할 수 없었다. 운영 델타에는 행위가 없다 — 비운다.\n */\nexport function actionOf(envelope: any): string | undefined {\n const a = (envelope?.data ?? envelope ?? {}).action\n return a === 'ADD' || a === 'OBSERVE' || a === 'DELETE' ? a : undefined\n}\n\n/** 한 이벤트에서 승격 키 전부 — 기록 경로가 이 함수 하나만 부른다. */\nexport function twinEventKeys(envelope: any): TwinEventKeys {\n return {\n action: actionOf(envelope),\n bizStep: clip(bizStepOf(envelope), 'bizStep'),\n epc: clip(epcOf(envelope), 'epc'),\n orderId: clip(orderOf(envelope), 'orderId'),\n locationId: clip(locationOf(envelope), 'locationId'),\n moverId: clip(equipmentIdOf(envelope), 'moverId')\n }\n}\n"]}
1
+ {"version":3,"file":"twin-event-keys.js","sourceRoot":"","sources":["../../../server/service/twin-event/twin-event-keys.ts"],"names":[],"mappings":"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","sourcesContent":["import { twinWarn } from '../../engine/log.js'\n/*\n * 저널 검색 키 추출 — **순수**. 인제스트가 기록할 때 한 번 뽑아 인덱스 가능한 실컬럼으로 승격한다.\n *\n * ── 왜 승격하는가 ───────────────────────────────────────────────────────────\n * 사용자가 저널에서 실제로 찾는 것은 \"이 팔레트의 이력\", \"이 오더가 어디까지 갔나\", \"이 도크에서\n * 무슨 일이 있었나\" 다. 그런데 그 값들은 전부 `payload`(simple-json = TEXT) **안**에 있었다.\n * things-factory 는 5개 DB 드라이버를 지원해야 해서 DB별 JSON 연산자를 쓸 수 없다 —\n * 즉 승격 없이는 **어떤 방법으로도 서버에서 그 조건으로 거를 수 없었다**. 클라이언트가 받아온\n * 몇 천 건 안에서만 찾는 시늉이 최선이었고, 저널이 커질수록 그 시늉은 거짓말에 가까워진다.\n *\n * 그래서 검색 축이 되는 값만 골라 컬럼으로 꺼낸다. payload 는 그대로 둔다(정본은 여전히 payload —\n * 이건 파생 색인이지 새로운 진실이 아니다).\n *\n * ── 왜 여기(순수 모듈)인가 ──────────────────────────────────────────────────\n * 기록 경로가 둘이다(`persistBatch` 라이브 벌크 · `persist` 심 단건). 두 곳에 각자 적으면\n * 반드시 어긋나고, 어긋난 색인은 \"없는 것처럼 보이는 이벤트\" 를 만든다 — 저널에서 가장 나쁜 결함이다.\n */\n\n/** 승격된 검색 키 — 전부 선택적. 뽑히지 않으면 **빈 문자열이 아니라 undefined**(결측≠빈값). */\nexport interface TwinEventKeys {\n action?: string\n bizStep?: string\n epc?: string\n orderId?: string\n locationId?: string\n moverId?: string\n}\n\n/*\n * 컬럼 길이 상한. GS1 식별자(EPC URN·GDTI·SGLN)는 규격상 이보다 훨씬 짧다.\n * 넘치는 값이 오면 **알리지 않고 자르지 않고** 경고를 남긴다 — 색인이 원본과 다르면 검색 결과가 거짓이 되는데,\n * 그 사실이 어디에도 안 남으면 아무도 모른다.\n */\nconst MAX_KEY = 255\n\nfunction clip(v: unknown, field: string): string | undefined {\n if (v === undefined || v === null) return undefined\n const s = String(v)\n if (!s) return undefined\n if (s.length <= MAX_KEY) return s\n twinWarn(\n `[twin-event-keys] ${field} exceeds ${MAX_KEY} chars and was clipped for indexing — ` +\n `search on this value may be incomplete. payload keeps the full value. (${s.slice(0, 60)}…)`\n )\n return s.slice(0, MAX_KEY)\n}\n\n/** CBV bizStep URN 의 끝마디. 없으면 이벤트 타입에서 유추(`epcis.` 접두 제거). */\nexport function bizStepOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n const tail = String(d.bizStep ?? '').split(':').pop()\n return tail || String(envelope?.eventType ?? '').replace('epcis.', '') || undefined\n}\n\n/**\n * 품목 식별자 — **전체 값**을 저장한다(끝마디만 저장하지 않는다).\n *\n * 표시용 축약은 화면이 하고, 색인은 원본을 갖는다. `search` 는 부분일치(contains)라\n * 전체를 저장해 두면 끝마디(\"402.2\")로도 URN 전체로도 찾힌다. 반대로 끝마디만 저장하면\n * URN 으로 찾는 경로가 사라진다.\n */\nexport function epcOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n return d.epcList?.[0] ?? d.parentID ?? d.quantityList?.[0]?.epcClass ?? undefined\n}\n\n/**\n * 오더 식별자 — 운영 델타의 `orderId` 우선, EPCIS 는 `bizTransactionList`.\n *\n * ── 실측 (2026-08-24) — 이 컬럼이 **전부 비어 있었다** ──────────────────────\n * 여기가 찾던 이름이 `d.order` 였다. 그런데 커널이 내는 이름은 **`orderId`** 다\n * (`OrderStatusDelta.orderId` · `TaskStatusDelta.orderId`). `d.order` 를 내는 코드는 커널에 **한 곳도\n * 없다** — 죽은 가지였다. 그래서 운영 델타는 이 컬럼을 한 번도 채우지 못했다.\n *\n * order.status 29,403,565 행 — order_id 비어 있음 29,403,565 (100%)\n * task.status 191,175 행 — 비어 있음 191,175 (100%)\n *\n * 그 결과 색인 `ix_twin_event_4` 가 **자기 주석이 적어 둔 용도**(「이 오더가 어디까지 갔나」)로 쓸 수\n * 없었다. 화면은 오더를 눌러도 「연결된 이벤트가 없습니다」를 냈고, 그 답은 질의 결과로는 정직했다 —\n * 시점을 어디로 옮겨도 0 이었다.\n *\n * 순서가 중요하다: 운영 델타가 먼저다. EPCIS 의 `bizTransactionList` 는 **거래**(PO/SO)이고 오더와\n * 같은 것이 아닐 수 있으므로, 오더를 스스로 말하는 사건은 그 말을 그대로 쓴다.\n *\n * **과거 행은 채워지지 않는다**(사용자 결정 2026-08-24, `moverId` 때와 같은 방식). 앞으로 들어오는\n * 사건부터 조회된다.\n */\nexport function orderOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n return d.orderId ?? d.bizTransactionList?.[0]?.bizTransaction ?? undefined\n}\n\n/**\n * 위치 — EPCIS 는 읽은 지점(readPoint) 우선, 없으면 업무 위치(bizLocation).\n * 운영 델타(무버 이동 등)는 그 둘이 없고 평범한 `location` 을 쓴다 — 빠뜨리면 설비가 어디서\n * 무엇을 했는지가 위치 축에서 통째로 사라진다.\n */\nexport function locationOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n return d.readPoint?.id ?? d.bizLocation?.id ?? d.location ?? undefined\n}\n\n/**\n * 설비·무버 — 운영 델타(equipment.status·task.status)가 대상을 가리키는 축.\n *\n * EPCIS 어휘가 아니라서 다른 축 어디에도 안 잡힌다. 이게 없으면 \"이 지게차가 오늘 무엇을 했나\" 를\n * 서버에서 물을 방법이 없어, 화면이 저널을 통째로 받아 전수 조회하는 수밖에 없다.\n */\nexport function equipmentIdOf(envelope: any): string | undefined {\n const d = envelope?.data ?? envelope ?? {}\n /*\n * ── 실측 (2026-08-24) — 두 갈래를 놓치고 있었다 ────────────────────────────\n * 위 주석이 `task.status` 를 출처로 **적어 두었는데** 이 함수는 `d.moverId` 만 봤다. 작업이 자원을\n * 가리키는 이름은 `resourceRef` 다(`TaskStatusDelta.resourceRef`) — 그래서 작업 191,175 행 전부\n * 이 컬럼이 비었고, 「이 지게차가 오늘 무엇을 했나」에서 **작업이 통째로 빠졌다.**\n *\n * 그리고 에너지 사건은 `equipmentId` 를 쓴다(어휘가 `movers`→`equipment` 로 개명된 뒤에 생긴\n * 채널이다). 실측 `energy.equipment` 31,079 행 전부 비어 있었다 — 「이 설비가 얼마를 먹었나」를\n * 설비 축으로 물을 수 없었다.\n *\n * 셋을 함께 본다. 개명 세대가 섞여 있는 것은 저널의 성질이고, 읽는 쪽이 그것을 흡수한다.\n */\n return d.moverId ?? d.resourceRef ?? d.equipmentId ?? undefined\n}\n\n/**\n * EPCIS 행위 — `ADD`·`OBSERVE`·`DELETE`. 세 값이 아니면 `undefined`(원천의 잡값을 색인에 넣지 않는다).\n *\n * 이 키는 검색 축이 아니라 **판별식**이다: 되풀어 읽는 스냅샷은 원리적으로 `DELETE` 를 못 낸다.\n * 그래서 「이 트윈이 반출을 한 번이라도 계산했는가」가 진짜 delta 피드의 증거인데, payload 안에 있는\n * 동안에는 5개 드라이버 어디에서도 그 질문을 할 수 없었다. 운영 델타에는 행위가 없다 — 비운다.\n */\nexport function actionOf(envelope: any): string | undefined {\n const a = (envelope?.data ?? envelope ?? {}).action\n return a === 'ADD' || a === 'OBSERVE' || a === 'DELETE' ? a : undefined\n}\n\n/** 한 이벤트에서 승격 키 전부 — 기록 경로가 이 함수 하나만 부른다. */\nexport function twinEventKeys(envelope: any): TwinEventKeys {\n return {\n action: actionOf(envelope),\n bizStep: clip(bizStepOf(envelope), 'bizStep'),\n epc: clip(epcOf(envelope), 'epc'),\n orderId: clip(orderOf(envelope), 'orderId'),\n locationId: clip(locationOf(envelope), 'locationId'),\n moverId: clip(equipmentIdOf(envelope), 'moverId')\n }\n}\n"]}
@@ -5,4 +5,5 @@ export declare class TwinEventList {
5
5
  capped?: boolean;
6
6
  countCap?: number;
7
7
  nextCursor?: string;
8
+ oldestRevision?: number;
8
9
  }
@@ -8,7 +8,7 @@ const twin_event_js_1 = require("./twin-event.js");
8
8
  * 저널 목록 반환형 — things-factory 표준 `{ items, total }`(AttributeSetList·DomainList 등과 동형).
9
9
  *
10
10
  * `total` 이 이 타입의 존재 이유다. 기존 `twinEvents` 는 배열만 돌려줘서 **화면이 자기가 전체를 받은
11
- * 건지 잘린 건지 알 방법이 없었다** — 그래서 리스트가 조용히 잘린 채로 "이게 전부" 처럼 보였다.
11
+ * 건지 잘린 건지 알 방법이 없었다** — 그래서 리스트가 알리지 않고 잘린 채로 "이게 전부" 처럼 보였다.
12
12
  * 총건수를 함께 주면 화면은 "48 / 12,904" 라고 정직하게 말할 수 있고, 사용자는 좁혀야 한다는 걸 안다.
13
13
  */
14
14
  let TwinEventList = class TwinEventList {
@@ -43,6 +43,13 @@ tslib_1.__decorate([
43
43
  (0, type_graphql_1.Field)({ nullable: true, description: 'Opaque cursor for the next page. Pass it back as pagination.after to continue exactly where this page ended, without the duplicate or skipped rows that offset paging produces on a journal that keeps growing at the head. Null when this page is the last one.' }),
44
44
  tslib_1.__metadata("design:type", String)
45
45
  ], TwinEventList.prototype, "nextCursor", void 0);
46
+ tslib_1.__decorate([
47
+ (0, type_graphql_1.Field)(type => type_graphql_1.Int, {
48
+ nullable: true,
49
+ description: 'The oldest revision still stored for this twin. When it is greater than 1, older events were deleted by the journal retention policy: the screen must say the record starts here rather than implying the twin began at this point. NULL when the caller asked for more than one twin (the axis is per twin) or when nothing is stored.'
50
+ }),
51
+ tslib_1.__metadata("design:type", Number)
52
+ ], TwinEventList.prototype, "oldestRevision", void 0);
46
53
  exports.TwinEventList = TwinEventList = tslib_1.__decorate([
47
54
  (0, type_graphql_1.ObjectType)({ description: 'A page of twin journal events together with the total number of matching records.' })
48
55
  ], TwinEventList);
@@ -1 +1 @@
1
- {"version":3,"file":"twin-event-type.js","sourceRoot":"","sources":["../../../server/service/twin-event/twin-event-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAqD;AAErD,mDAA2C;AAE3C;;;;;;GAMG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAa;CAsCzB,CAAA;AAtCY,sCAAa;AAExB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,yBAAS,CAAC,EAAE,EAAE,WAAW,EAAE,6FAA6F,EAAE,CAAC;;4CACzH;AAclB;IALC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,iTAAiT;KACpT,CAAC;;4CACY;AAOd;IALC,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,mMAAmM;KACtM,CAAC;;6CACc;AAMhB;IAJC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,6HAA6H;KAC3I,CAAC;;+CACe;AAQjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kQAAkQ,EAAE,CAAC;;iDACxR;wBArCR,aAAa;IADzB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,mFAAmF,EAAE,CAAC;GACpG,aAAa,CAsCzB","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\n\nimport { TwinEvent } from './twin-event.js'\n\n/*\n * 저널 목록 반환형 — things-factory 표준 `{ items, total }`(AttributeSetList·DomainList 등과 동형).\n *\n * `total` 이 이 타입의 존재 이유다. 기존 `twinEvents` 는 배열만 돌려줘서 **화면이 자기가 전체를 받은\n * 건지 잘린 건지 알 방법이 없었다** — 그래서 리스트가 조용히 잘린 채로 \"이게 전부\" 처럼 보였다.\n * 총건수를 함께 주면 화면은 \"48 / 12,904\" 라고 정직하게 말할 수 있고, 사용자는 좁혀야 한다는 걸 안다.\n */\n@ObjectType({ description: 'A page of twin journal events together with the total number of matching records.' })\nexport class TwinEventList {\n @Field(type => [TwinEvent], { description: 'The events on this page, ordered by the requested sorting (revision descending by default).' })\n items: TwinEvent[]\n\n /*\n * 전체 수 — **상한을 넘으면 비운다.**\n *\n * 세는 데도 값이 든다(2026-08-22 실측: 저널 358만 행인 트윈에서 전량 세기 5.32초 · 상한 1만까지\n * 0.04초). 그래서 상한까지만 세고, 넘으면 「모른다」로 둔다. 그때 상한값을 전체로 적으면 화면이\n * 거짓을 말한다 — 없는 것과 모르는 것을 가르는 것이 이 필드의 규율이다.\n */\n @Field(type => Int, {\n nullable: true,\n description:\n 'Total number of events matching the filters, ignoring pagination. Lets the caller show an honest \"shown of total\" count instead of silently truncating. NULL when the server stopped counting at countCap: the exact total is then unknown, and capped says so — do not render the cap as if it were the total.'\n })\n total?: number\n\n @Field({\n nullable: true,\n description:\n 'True when there are more matching events than countCap, so total could not be established. The caller should say \"first N shown\" and offer narrowing rather than implying it received everything.'\n })\n capped?: boolean\n\n @Field(type => Int, {\n nullable: true,\n description: 'The counting limit the server applied. Reported so the screen can say how far the count is trustworthy instead of guessing.'\n })\n countCap?: number\n\n /*\n * 다음 페이지 커서. 저널은 **머리에 계속 쌓이는 목록**이라 offset 으로 뒤를 읽으면\n * 읽는 사이 들어온 이벤트만큼 밀려 본 행이 또 나오거나 못 본 행이 사라진다.\n * 이 값을 그대로 다음 요청의 `pagination.after` 로 돌려주면 그 문제가 없다.\n */\n @Field({ nullable: true, description: 'Opaque cursor for the next page. Pass it back as pagination.after to continue exactly where this page ended, without the duplicate or skipped rows that offset paging produces on a journal that keeps growing at the head. Null when this page is the last one.' })\n nextCursor?: string\n}\n"]}
1
+ {"version":3,"file":"twin-event-type.js","sourceRoot":"","sources":["../../../server/service/twin-event/twin-event-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAqD;AAErD,mDAA2C;AAE3C;;;;;;GAMG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAa;CAuDzB,CAAA;AAvDY,sCAAa;AAExB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,yBAAS,CAAC,EAAE,EAAE,WAAW,EAAE,6FAA6F,EAAE,CAAC;;4CACzH;AAclB;IALC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,iTAAiT;KACpT,CAAC;;4CACY;AAOd;IALC,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,mMAAmM;KACtM,CAAC;;6CACc;AAMhB;IAJC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EAAE,6HAA6H;KAC3I,CAAC;;+CACe;AAQjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kQAAkQ,EAAE,CAAC;;iDACxR;AAiBnB;IALC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,yUAAyU;KAC5U,CAAC;;qDACqB;wBAtDZ,aAAa;IADzB,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,mFAAmF,EAAE,CAAC;GACpG,aAAa,CAuDzB","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\n\nimport { TwinEvent } from './twin-event.js'\n\n/*\n * 저널 목록 반환형 — things-factory 표준 `{ items, total }`(AttributeSetList·DomainList 등과 동형).\n *\n * `total` 이 이 타입의 존재 이유다. 기존 `twinEvents` 는 배열만 돌려줘서 **화면이 자기가 전체를 받은\n * 건지 잘린 건지 알 방법이 없었다** — 그래서 리스트가 알리지 않고 잘린 채로 \"이게 전부\" 처럼 보였다.\n * 총건수를 함께 주면 화면은 \"48 / 12,904\" 라고 정직하게 말할 수 있고, 사용자는 좁혀야 한다는 걸 안다.\n */\n@ObjectType({ description: 'A page of twin journal events together with the total number of matching records.' })\nexport class TwinEventList {\n @Field(type => [TwinEvent], { description: 'The events on this page, ordered by the requested sorting (revision descending by default).' })\n items: TwinEvent[]\n\n /*\n * 전체 수 — **상한을 넘으면 비운다.**\n *\n * 세는 데도 값이 든다(2026-08-22 실측: 저널 358만 행인 트윈에서 전량 세기 5.32초 · 상한 1만까지\n * 0.04초). 그래서 상한까지만 세고, 넘으면 「모른다」로 둔다. 그때 상한값을 전체로 적으면 화면이\n * 거짓을 말한다 — 없는 것과 모르는 것을 가르는 것이 이 필드의 규율이다.\n */\n @Field(type => Int, {\n nullable: true,\n description:\n 'Total number of events matching the filters, ignoring pagination. Lets the caller show an honest \"shown of total\" count instead of silently truncating. NULL when the server stopped counting at countCap: the exact total is then unknown, and capped says so — do not render the cap as if it were the total.'\n })\n total?: number\n\n @Field({\n nullable: true,\n description:\n 'True when there are more matching events than countCap, so total could not be established. The caller should say \"first N shown\" and offer narrowing rather than implying it received everything.'\n })\n capped?: boolean\n\n @Field(type => Int, {\n nullable: true,\n description: 'The counting limit the server applied. Reported so the screen can say how far the count is trustworthy instead of guessing.'\n })\n countCap?: number\n\n /*\n * 다음 페이지 커서. 저널은 **머리에 계속 쌓이는 목록**이라 offset 으로 뒤를 읽으면\n * 읽는 사이 들어온 이벤트만큼 밀려 본 행이 또 나오거나 못 본 행이 사라진다.\n * 이 값을 그대로 다음 요청의 `pagination.after` 로 돌려주면 그 문제가 없다.\n */\n @Field({ nullable: true, description: 'Opaque cursor for the next page. Pass it back as pagination.after to continue exactly where this page ended, without the duplicate or skipped rows that offset paging produces on a journal that keeps growing at the head. Null when this page is the last one.' })\n nextCursor?: string\n\n /*\n * **앞부분이 지워졌다** — 보관 기간 정리가 이 트윈의 오래된 사건을 지운 경우 (2026-08-27).\n *\n * 정리는 선언된 보관 기간(기본 7일)보다 오래된 사건을 지운다. 그러면 이 목록의 첫 페이지 뒤로 더\n * 내려가도 어느 지점에서 기록이 끝난다. 화면이 그것을 말하지 않으면 사람은 「이 트윈은 그때부터\n * 시작했다」로 읽는다. 없었던 것과 지워진 것은 다른 사실이다.\n *\n * 값은 저장하지 않고 조회할 때 구한다(남아 있는 가장 작은 리비전). 저장하면 실제 데이터와 어긋날 수\n * 있고, 어긋나면 어느 쪽이 사실인지 알 수 없다.\n */\n @Field(type => Int, {\n nullable: true,\n description:\n 'The oldest revision still stored for this twin. When it is greater than 1, older events were deleted by the journal retention policy: the screen must say the record starts here rather than implying the twin began at this point. NULL when the caller asked for more than one twin (the axis is per twin) or when nothing is stored.'\n })\n oldestRevision?: number\n}\n"]}
@@ -10,8 +10,8 @@ export declare class TwinEvent {
10
10
  * 이 사실이 **어느 공장에서** 일어났나 — `TwinStructure.rev`.
11
11
  *
12
12
  * 이것이 없던 시절에는 구조가 바뀌면 저널을 통째로 지우는 것이 유일한 길이었다(안 지우면 옛
13
- * 이벤트를 새 공장에 대고 접어 이력이 거짓말을 한다). 이 한 칸이 셋째 길을 연다 — 재생이
14
- * 구조가 바뀌는 지점에서 전환한 뒤 이어 접는다.
13
+ * 이벤트를 새 공장에 대고 계산해 이력이 거짓말을 한다). 이 한 칸이 셋째 길을 연다 — 재생이
14
+ * 구조가 바뀌는 지점에서 전환한 뒤 이어서 계산한다.
15
15
  *
16
16
  * **비어 있을 수 있다**: 컬럼이 생기기 전에 쓰인 행이다. 그 행들은 **가장 오래된 리비전**에
17
17
  * 속한다(0 을 채워 넣지 않는다 — 0 은 "모름" 이 아니라 유효한 번호처럼 보인다).
@@ -75,11 +75,11 @@ tslib_1.__decorate([
75
75
  * payload 가 여전히 정본이고 이것들은 **파생 색인**이다 — 새로운 진실이 아니라 찾을 수 있게 하는 장치.
76
76
  * simple-json(TEXT) 안의 값은 5개 DB 드라이버 공통으로 거를 방법이 없어서(멀티DB 호환 규칙상
77
77
  * DB별 JSON 연산자 금지) 승격 외의 선택지가 없다.
78
- * 길이 상한 255 는 GS1 식별자 규격 대비 충분하며, 넘치는 값은 조용히 잘리지 않고 경고를 남긴다.
78
+ * 길이 상한 255 는 GS1 식별자 규격 대비 충분하며, 넘치는 값은 알리지 않고 잘리지 않고 경고를 남긴다.
79
79
  *
80
80
  * ── 축을 새로 추가할 때 ──────────────────────────────────────────────────
81
81
  * 새 승격 컬럼을 만들면 **이미 쌓인 행은 비어 있다.** 그 행들은 새 축으로 검색되지 않고,
82
- * 사용자에게는 "과거가 없는 것" 으로 보인다(조용히 빠진 데이터).
82
+ * 사용자에게는 "과거가 없는 것" 으로 보인다(알리지 않고 빠진 데이터).
83
83
  *
84
84
  * 예전에는 기동 훅이 매번 테이블을 살펴 채웠다. 그건 두 가지로 틀렸다 — 저널이 커질수록 부팅이
85
85
  * 느려지고, "bizStep 이 비었으면 옛 행" 이라는 전제를 코드에 고정해 그 전제가 깨지는 순간
@@ -154,9 +154,37 @@ exports.TwinEvent = TwinEvent = tslib_1.__decorate([
154
154
  /* ix_5 — "이 지시가 무엇을 일으켰나". 감사 기록의 commandId 로 저널을 다시 계산하는 축. */
155
155
  ,
156
156
  (0, typeorm_1.Index)('ix_twin_event_5', (e) => [e.domain, e.correlationId], { unique: false })
157
- /* ix_6 — "반출을 한 번이라도 접었나". 없으면 그 질문이 인스턴스 저널 전량 주사가 된다. */
157
+ /* ix_6 — "반출을 한 번이라도 계산했나". 없으면 그 질문이 인스턴스 저널 전량 주사가 된다. */
158
158
  ,
159
- (0, typeorm_1.Index)('ix_twin_event_6', (e) => [e.domain, e.instanceId, e.action], { unique: false }),
159
+ (0, typeorm_1.Index)('ix_twin_event_6', (e) => [e.domain, e.instanceId, e.action], { unique: false })
160
+ /*
161
+ * ix_7 — **"이 종류의 사실을 시각순으로"** (2026-08-25 실측으로 추가)
162
+ *
163
+ * 이 축이 없어서 공정 타임라인이 22초였다. 종류로 거르고 시각으로 정렬하는 조회가 갈 곳이 없었다:
164
+ *
165
+ * ix_2 (…, eventType, revision) 종류는 걸러지고 정렬이 **적힌 순서**다 → 4월 사실을 최근으로 그렸다
166
+ * ix_1 (…, eventTime) 정렬은 맞고 **종류를 못 거른다** → 행마다 테이블을 열어 확인한다
167
+ *
168
+ * 뒤쪽으로 걸으면 그 트윈에서 찾는 종류가 멈춘 시점까지의 모든 행을 읽고 버린다. 승화푸드에서 작업
169
+ * 사실은 8월 22일에 멈춰 있고 그 뒤로 리비전이 110만 늘었으므로, 200건에 닿기까지 110만 행을 열었다.
170
+ *
171
+ * 위 주석이 「측정을 근거로 추가할 일이지 지레 얹을 일은 아니다」라고 적어 두었다. 그 측정이 나왔다.
172
+ */
173
+ ,
174
+ (0, typeorm_1.Index)('ix_twin_event_7', (e) => [e.domain, e.instanceId, e.eventType, e.eventTime], { unique: false })
175
+ /*
176
+ * ix_8 — **"이 트윈의 오래된 기록"** (2026-08-27 실측으로 추가)
177
+ *
178
+ * 보관 기간 정리가 `created_at` 으로 자르는데(보관은 저장 나이의 문제다) 그 축에 인덱스가 없었다.
179
+ * 그래서 정리가 매 배치마다 그 트윈의 저널을 전량 주사했다.
180
+ *
181
+ * hatio-mx2 의 7일 밖 행을 세기 208초 ← 1,029만 행을 전부 읽는다(7일 밖은 2,446건뿐이다)
182
+ * revision 으로 한 행 짚기 0.004초
183
+ *
184
+ * 정리는 한 창에 2초만 쓰도록 예산이 걸려 있어서, 그 2초를 주사에 다 쓰고 44건만 지우고 멈췄다.
185
+ */
186
+ ,
187
+ (0, typeorm_1.Index)('ix_twin_event_8', (e) => [e.domain, e.instanceId, e.createdAt], { unique: false }),
160
188
  (0, type_graphql_1.ObjectType)({ description: 'Append-only twin event journal record (EPCIS event or operational delta).' })
161
189
  ], TwinEvent);
162
190
  //# sourceMappingURL=twin-event.js.map