@things-factory/headless-twin 10.0.15 → 10.0.18

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 (513) 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 +18 -82
  5. package/dist-server/engine/canonical-ingest.js +183 -18
  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 +4 -4
  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 +90 -10
  17. package/dist-server/engine/ingest-health.js +125 -16
  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 +212 -14
  25. package/dist-server/engine/kpi-fold.js +374 -21
  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 +281 -36
  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 +15 -14
  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 +3 -3
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +6 -5
  54. package/dist-server/engine/oee-accumulator.js +6 -5
  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 +307 -54
  78. package/dist-server/engine/twin-engine.js +822 -196
  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 +30 -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 +116 -9
  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 +76 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +25 -16
  133. package/dist-server/service/reference/reference-master.js +35 -13
  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 +12 -12
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/template-registry.d.ts +1 -1
  139. package/dist-server/service/reference/template-registry.js.map +1 -1
  140. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  141. package/dist-server/service/reference/twin-reference.js.map +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  143. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  144. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  145. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  146. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  149. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  150. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  151. package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
  152. package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
  153. package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
  154. package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
  155. package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
  156. package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
  157. package/dist-server/service/twin-backfill/index.d.ts +2 -0
  158. package/dist-server/service/twin-backfill/index.js +6 -0
  159. package/dist-server/service/twin-backfill/index.js.map +1 -0
  160. package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
  161. package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
  162. package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
  163. package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
  164. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  165. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  166. package/dist-server/service/twin-event/twin-event-keys.js +2 -2
  167. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  168. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  169. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  170. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  171. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  172. package/dist-server/service/twin-event/twin-event.js +32 -4
  173. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  174. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  175. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  176. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  177. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  178. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  179. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  180. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  181. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  182. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  183. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  184. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  185. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  186. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  187. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  188. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  189. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  190. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  191. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  192. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  193. package/dist-server/service/twin-journal/journal-order.js +17 -0
  194. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  195. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  196. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  197. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  198. package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
  199. package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
  200. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  201. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  202. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  203. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  204. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  205. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  206. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  207. package/dist-server/service/twin-model/epcis-coverage.js +34 -3
  208. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  209. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  210. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  211. package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
  212. package/dist-server/service/twin-model/isa95-coverage.js +33 -9
  213. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  214. package/dist-server/service/twin-model/item-ref.js +2 -2
  215. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  216. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  217. package/dist-server/service/twin-model/name-index.js +2 -2
  218. package/dist-server/service/twin-model/name-index.js.map +1 -1
  219. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  220. package/dist-server/service/twin-model/project-structure.js +7 -6
  221. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  222. package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
  223. package/dist-server/service/twin-model/twin-equipment.js +3 -3
  224. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  225. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  226. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  227. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  228. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  229. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  230. package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
  231. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  232. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  233. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  234. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  235. package/dist-server/service/twin-model/twin-model-query.js +51 -30
  236. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  237. package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
  238. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  239. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  240. package/dist-server/service/twin-model/twin-operation.js +1 -1
  241. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  242. package/dist-server/service/twin-space/move-space.js +2 -2
  243. package/dist-server/service/twin-space/move-space.js.map +1 -1
  244. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  245. package/dist-server/service/twin-space/representation-role.js +18 -0
  246. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  247. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  248. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  249. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  250. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  251. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  252. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  253. package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
  254. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  255. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  256. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  257. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  258. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  259. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  260. package/dist-server/service/twin-subject/index.d.ts +9 -0
  261. package/dist-server/service/twin-subject/index.js +16 -0
  262. package/dist-server/service/twin-subject/index.js.map +1 -0
  263. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  264. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  265. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  266. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  267. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  268. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  269. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  270. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  271. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  272. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  273. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  274. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  275. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  276. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  277. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  278. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  279. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  280. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  281. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  282. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  283. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  284. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  285. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  286. package/dist-shared/entity-delta.d.ts +1 -1
  287. package/dist-shared/entity-delta.js +4 -4
  288. package/dist-shared/entity-delta.js.map +1 -1
  289. package/dist-shared/touched-items.js +2 -2
  290. package/dist-shared/touched-items.js.map +1 -1
  291. package/dist-shared/twin-level.js +1 -1
  292. package/dist-shared/twin-level.js.map +1 -1
  293. package/package.json +7 -6
  294. package/server/engine/attention-digest.ts +9 -9
  295. package/server/engine/canonical-ingest.ts +202 -87
  296. package/server/engine/command-routing.ts +1 -1
  297. package/server/engine/declared-stimulus.ts +1 -1
  298. package/server/engine/energy-topology.ts +2 -2
  299. package/server/engine/ingest-dedupe.ts +149 -0
  300. package/server/engine/ingest-health.ts +171 -22
  301. package/server/engine/integration-probes.ts +7 -7
  302. package/server/engine/kpi-baseline.ts +4 -4
  303. package/server/engine/kpi-fold.ts +592 -41
  304. package/server/engine/kpi-query.ts +307 -48
  305. package/server/engine/live-feed-registry.ts +1 -1
  306. package/server/engine/load-meter.ts +1 -1
  307. package/server/engine/local-declarations.ts +15 -14
  308. package/server/engine/log.ts +1 -1
  309. package/server/engine/measured-estimator.ts +1 -1
  310. package/server/engine/measured-yield.ts +1 -1
  311. package/server/engine/model-basis.ts +2 -2
  312. package/server/engine/model-gap.ts +3 -3
  313. package/server/engine/model-vocabulary.ts +2 -2
  314. package/server/engine/oee-accumulator.ts +8 -6
  315. package/server/engine/property-effects.ts +2 -2
  316. package/server/engine/read-time.ts +77 -0
  317. package/server/engine/restart-policy.ts +1 -1
  318. package/server/engine/runtime-key.ts +1 -1
  319. package/server/engine/spec-coverage.ts +3 -3
  320. package/server/engine/stage-path.ts +5 -5
  321. package/server/engine/state-axes.ts +2 -2
  322. package/server/engine/travel-estimator.ts +2 -2
  323. package/server/engine/twin-engine.ts +906 -205
  324. package/server/engine/warm-start.ts +6 -6
  325. package/server/index.ts +1 -1
  326. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  327. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  328. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  329. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  330. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  331. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  332. package/server/migrations/index.ts +7 -1
  333. package/server/routes.ts +73 -5
  334. package/server/service/index.ts +7 -1
  335. package/server/service/reference/control-routing.ts +1 -1
  336. package/server/service/reference/discovery-result.ts +3 -3
  337. package/server/service/reference/hook-contract.ts +103 -0
  338. package/server/service/reference/ingest-space.ts +4 -4
  339. package/server/service/reference/knob-defaults.ts +4 -4
  340. package/server/service/reference/reference-adapter.ts +145 -10
  341. package/server/service/reference/reference-hook.ts +145 -0
  342. package/server/service/reference/reference-live.ts +87 -15
  343. package/server/service/reference/reference-master.ts +51 -20
  344. package/server/service/reference/reference-resolver.ts +11 -11
  345. package/server/service/reference/template-registry.ts +1 -1
  346. package/server/service/reference/twin-reference.ts +3 -3
  347. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  348. package/server/service/twin-audit/command-audit.ts +1 -1
  349. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  350. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  351. package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
  352. package/server/service/twin-backfill/backfill-shape.ts +86 -0
  353. package/server/service/twin-backfill/index.ts +3 -0
  354. package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
  355. package/server/service/twin-control/twin-control-mutation.ts +2 -2
  356. package/server/service/twin-event/twin-event-keys.ts +2 -2
  357. package/server/service/twin-event/twin-event-type.ts +18 -1
  358. package/server/service/twin-event/twin-event.ts +32 -6
  359. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  360. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  361. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  362. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  363. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  364. package/server/service/twin-instance/twin-instance.ts +4 -4
  365. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  366. package/server/service/twin-journal/event-type-split.ts +144 -0
  367. package/server/service/twin-journal/journal-order.ts +20 -0
  368. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  369. package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
  370. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
  371. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  372. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  373. package/server/service/twin-model/epcis-coverage.ts +34 -3
  374. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  375. package/server/service/twin-model/isa95-coverage.ts +43 -16
  376. package/server/service/twin-model/item-ref.ts +2 -2
  377. package/server/service/twin-model/name-index.ts +2 -2
  378. package/server/service/twin-model/project-structure.ts +7 -6
  379. package/server/service/twin-model/twin-equipment.ts +16 -4
  380. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  381. package/server/service/twin-model/twin-location.ts +1 -1
  382. package/server/service/twin-model/twin-model-item-query.ts +15 -15
  383. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  384. package/server/service/twin-model/twin-model-query.ts +49 -28
  385. package/server/service/twin-model/twin-model-tree-query.ts +26 -2
  386. package/server/service/twin-model/twin-operation.ts +2 -2
  387. package/server/service/twin-space/move-space.ts +3 -3
  388. package/server/service/twin-space/representation-role.ts +15 -0
  389. package/server/service/twin-space/space-integrity.ts +1 -1
  390. package/server/service/twin-space/twin-space-representation.ts +37 -0
  391. package/server/service/twin-space/twin-space-resolver.ts +40 -8
  392. package/server/service/twin-structure/twin-structure.ts +3 -3
  393. package/server/service/twin-subject/event-subjects.ts +155 -0
  394. package/server/service/twin-subject/index.ts +10 -0
  395. package/server/service/twin-subject/item-resolver.ts +92 -0
  396. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  397. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  398. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  399. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  400. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  401. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  402. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  403. package/shared/entity-delta.ts +4 -4
  404. package/shared/touched-items.ts +2 -2
  405. package/shared/twin-level.ts +1 -1
  406. package/test/adopt-structure-live.test.ts +3 -3
  407. package/test/aggregate-not-findone.test.ts +112 -0
  408. package/test/attention-digest.test.ts +6 -6
  409. package/test/axis-read.test.ts +6 -5
  410. package/test/backfill-shape.test.ts +89 -0
  411. package/test/boot-resume.test.ts +20 -7
  412. package/test/broadcast-cost-baseline.test.ts +2 -1
  413. package/test/broadcast-period.test.ts +2 -2
  414. package/test/broadcast-queue-overflow.test.ts +244 -0
  415. package/test/canonical-ingest-vocabularies.test.ts +259 -8
  416. package/test/canonical-quantity-door.test.ts +2 -2
  417. package/test/capability-mapping.test.ts +1 -1
  418. package/test/checkpoint-refuses-empty.test.ts +2 -2
  419. package/test/connector-capability-declaration.test.ts +88 -0
  420. package/test/contract-layer-guard.test.ts +48 -0
  421. package/test/control-capability.test.ts +1 -1
  422. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  423. package/test/declaration-reaches-model.test.ts +7 -5
  424. package/test/declared-stimulus.test.ts +4 -4
  425. package/test/discovery-result.test.ts +2 -2
  426. package/test/entity-delta.test.ts +4 -4
  427. package/test/equipment-identity-reaches.test.ts +57 -0
  428. package/test/event-subjects.test.ts +147 -0
  429. package/test/event-time-column.test.ts +3 -3
  430. package/test/event-type-split.test.ts +155 -0
  431. package/test/fact-scope-wiring.test.ts +96 -0
  432. package/test/fold-gap-speaks.test.ts +53 -0
  433. package/test/forecast-metrics.test.ts +1 -1
  434. package/test/forecast-tuning.test.ts +1 -1
  435. package/test/generated-not-counted.test.ts +99 -0
  436. package/test/ingest-bench.test.ts +2 -1
  437. package/test/ingest-health-engine.test.ts +2 -2
  438. package/test/ingest-health-wiring.test.ts +7 -5
  439. package/test/ingest-health.test.ts +6 -6
  440. package/test/ingest-history.test.ts +2 -2
  441. package/test/ingest-idempotent.test.ts +132 -0
  442. package/test/ingest-space.test.ts +3 -3
  443. package/test/ingest-wiring-guard.test.ts +197 -0
  444. package/test/integration-probes.test.ts +3 -3
  445. package/test/integration-runner.test.ts +1 -1
  446. package/test/item-ref.test.ts +1 -1
  447. package/test/journal-order-axis.test.ts +80 -0
  448. package/test/journal-read-discipline.test.ts +16 -6
  449. package/test/journal-retention.test.ts +171 -29
  450. package/test/journal-sort-axis.test.ts +1 -1
  451. package/test/journal-write-door.test.ts +3 -3
  452. package/test/journal-write-trend.test.ts +1 -1
  453. package/test/kernel-kind-guard.test.ts +3 -3
  454. package/test/knob-defaults.test.ts +1 -1
  455. package/test/kpi-baseline-db.test.ts +2 -2
  456. package/test/kpi-fold.test.ts +266 -14
  457. package/test/kpi-query-bench.test.ts +3 -3
  458. package/test/live-cursor-wiring.test.ts +6 -6
  459. package/test/live-kernel-facts.test.ts +11 -11
  460. package/test/live-mirror-parity.test.ts +9 -6
  461. package/test/load-meter.test.ts +1 -1
  462. package/test/local-declarations.test.ts +13 -12
  463. package/test/log-stamp.test.ts +1 -1
  464. package/test/master-to-twin.test.ts +2 -2
  465. package/test/measured-yield.test.ts +2 -2
  466. package/test/merge-streams.test.ts +70 -0
  467. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  468. package/test/model-basis.test.ts +2 -2
  469. package/test/model-gap.test.ts +4 -4
  470. package/test/model-vocabulary.test.ts +2 -2
  471. package/test/move-space.test.ts +1 -1
  472. package/test/oee-accumulator.test.ts +20 -5
  473. package/test/operational-vocabulary.test.ts +2 -2
  474. package/test/project-structure-db.test.ts +2 -2
  475. package/test/projection-reaches-screen.test.ts +7 -2
  476. package/test/projection-reads-declared.test.ts +109 -0
  477. package/test/property-effects.test.ts +10 -6
  478. package/test/read-failure-ledger.test.ts +106 -0
  479. package/test/read-failure-visible.test.ts +7 -7
  480. package/test/read-time.test.ts +118 -0
  481. package/test/reference-grounding.test.ts +1 -1
  482. package/test/reference-hook.test.ts +86 -0
  483. package/test/registry-key-guard.test.ts +1 -1
  484. package/test/rename-twin.test.ts +81 -0
  485. package/test/restart-policy.test.ts +3 -3
  486. package/test/resync-origin-site.test.ts +3 -3
  487. package/test/revision-axis.test.ts +14 -8
  488. package/test/runtime-key.test.ts +3 -3
  489. package/test/scale-twin-bench.test.ts +2 -1
  490. package/test/snapshot-freshness.test.ts +4 -4
  491. package/test/source-outcome-audit.test.ts +1 -1
  492. package/test/space-integrity.test.ts +1 -1
  493. package/test/space-representation.test.ts +66 -0
  494. package/test/space-scope-includes-stopped.test.ts +55 -0
  495. package/test/spec-coverage.test.ts +1 -1
  496. package/test/standard-coverage.test.ts +19 -21
  497. package/test/status-tally.test.ts +1 -1
  498. package/test/structure-revision-db.test.ts +7 -7
  499. package/test/subject-fast-path.test.ts +148 -0
  500. package/test/subject-read-model.test.ts +175 -0
  501. package/test/touched-items.test.ts +2 -2
  502. package/test/twin-audit.test.ts +1 -1
  503. package/test/twin-event-keys.test.ts +1 -1
  504. package/test/twin-model-item-db.test.ts +5 -5
  505. package/test/twin-model-tree-db.test.ts +35 -2
  506. package/test/twin-origin-resync.test.ts +1 -1
  507. package/test/vocabulary-guard.test.ts +1 -1
  508. package/test/warm-start-seam.test.ts +1 -1
  509. package/test/warm-start.test.ts +6 -6
  510. package/test/withheld-door.test.ts +2 -2
  511. package/test/yield-loop.test.ts +4 -4
  512. package/tsconfig.shared.tsbuildinfo +1 -1
  513. package/tsconfig.tsbuildinfo +1 -1
@@ -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":"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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"]}
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"]}
@@ -0,0 +1,24 @@
1
+ export interface BackfillResult {
2
+ /** 저널에 새로 적은 수. */
3
+ written: number;
4
+ /** 이미 있어서 건너뛴 수 — 다시 읽어도 중복이 쌓이지 않는다는 뜻이다. */
5
+ skipped: number;
6
+ /** 받지 않은 것과 그 이유. 조용히 버리지 않는다. */
7
+ refused: {
8
+ record: unknown;
9
+ reason: string;
10
+ }[];
11
+ }
12
+ /**
13
+ * 지난 기록을 저널에 채운다.
14
+ *
15
+ * `records` 는 라이브와 **같은 모양**이다 — 커넥터가 다른 어휘를 배울 필요가 없다. 검증·갈림·id 만들기는
16
+ * 커널의 유입이 그대로 한다(§`ingestCanonicalRecords`).
17
+ */
18
+ export declare function backfillPeriodFacts(input: {
19
+ domainId: string;
20
+ instanceId: string;
21
+ records: unknown[];
22
+ /** 이 시각을 「지금」으로 본다 — 보존 판정의 기준. 시험이 시각을 정할 수 있게 받는다. */
23
+ nowMs?: number;
24
+ }): Promise<BackfillResult>;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.backfillPeriodFacts = backfillPeriodFacts;
4
+ const shell_1 = require("@things-factory/shell");
5
+ const twin_event_js_1 = require("../twin-event/twin-event.js");
6
+ const twin_engine_js_1 = require("../../engine/twin-engine.js");
7
+ const canonical_ingest_js_1 = require("../../engine/canonical-ingest.js");
8
+ const read_time_js_1 = require("../../engine/read-time.js");
9
+ const backfill_shape_js_1 = require("./backfill-shape.js");
10
+ /** 이 도메인의 보존 기간(일). 정책이 없으면 `undefined` — 그때는 오래됨을 판정하지 않는다. */
11
+ async function retentionDaysOf(domainId) {
12
+ const perDomain = twin_engine_js_1.TwinEngine.retentionDaysOf ? await twin_engine_js_1.TwinEngine.retentionDaysOf(domainId).catch(() => undefined) : undefined;
13
+ const days = perDomain ?? twin_engine_js_1.TwinEngine.JOURNAL_RETENTION_DAYS;
14
+ return days && days > 0 ? days : undefined;
15
+ }
16
+ /**
17
+ * 지난 기록을 저널에 채운다.
18
+ *
19
+ * `records` 는 라이브와 **같은 모양**이다 — 커넥터가 다른 어휘를 배울 필요가 없다. 검증·갈림·id 만들기는
20
+ * 커널의 유입이 그대로 한다(§`ingestCanonicalRecords`).
21
+ */
22
+ async function backfillPeriodFacts(input) {
23
+ const { domainId, instanceId, records } = input;
24
+ const nowMs = input.nowMs ?? Date.now();
25
+ const refused = [];
26
+ if (!Array.isArray(records) || !records.length)
27
+ return { written: 0, skipped: 0, refused };
28
+ /* 검증과 갈림은 라이브와 같은 것을 쓴다 — 두 벌이 되면 채운 사실과 관측한 사실이 다른 규칙을 지난다. */
29
+ const { accepted, rejected } = (0, canonical_ingest_js_1.ingestCanonicalRecords)(records, domainId, new Date(nowMs).toISOString(),
30
+ /* 어느 트윈의 사실인가 — 마감된 구간의 이름이 이것으로 갈린다(커널 §`SubjectBasis`). */
31
+ twin_engine_js_1.TwinEngine.factScope(domainId, instanceId));
32
+ for (const r of rejected)
33
+ refused.push({ record: r.record, reason: (r.errors ?? []).join(' · ') });
34
+ const days = await retentionDaysOf(domainId);
35
+ const judged = (0, backfill_shape_js_1.judgeBackfill)(accepted, { nowMs, retentionDays: days });
36
+ refused.push(...judged.refused);
37
+ const keep = judged.keep;
38
+ if (!keep.length) {
39
+ twin_engine_js_1.TwinEngine.recordIngestResult(domainId, instanceId, records.length, refused.map(r => ({ record: r.record, errors: [r.reason] })), nowMs, 0);
40
+ return { written: 0, skipped: 0, refused };
41
+ }
42
+ /*
43
+ * 이미 있는 것을 가려낸다 — 그 구간의 그 종류만 읽는다(색인이 받쳐 준다). 페이로드의 id 로 견주고,
44
+ * 저장소 제약에 기대지 않는다(드라이버 다섯).
45
+ */
46
+ const times = keep.map(e => (0, read_time_js_1.absoluteTime)(e.eventTime).getTime());
47
+ const fromAt = new Date(Math.min(...times));
48
+ const toAt = new Date(Math.max(...times));
49
+ const types = [...new Set(keep.map(e => String(e.eventType)))];
50
+ const existing = await (0, shell_1.getRepository)(twin_event_js_1.TwinEvent)
51
+ .createQueryBuilder('e')
52
+ .select(['e.payload AS "payload"'])
53
+ .where('e.domain = :domainId', { domainId })
54
+ .andWhere('e.instanceId = :instanceId', { instanceId })
55
+ .andWhere('e.eventType IN (:...types)', { types })
56
+ .andWhere('e.eventTime BETWEEN :fromAt AND :toAt', { fromAt, toAt })
57
+ .getRawMany();
58
+ const seen = new Set();
59
+ for (const row of existing) {
60
+ const p = typeof row.payload === 'string' ? safeParse(row.payload) : row.payload;
61
+ const id = p?.eventId;
62
+ if (typeof id === 'string' && id)
63
+ seen.add(id);
64
+ }
65
+ const { fresh, skipped } = (0, backfill_shape_js_1.splitAlreadyThere)(keep, seen);
66
+ if (!fresh.length) {
67
+ twin_engine_js_1.TwinEngine.recordIngestResult(domainId, instanceId, records.length, refused.map(r => ({ record: r.record, errors: [r.reason] })), nowMs, skipped);
68
+ return { written: 0, skipped, refused };
69
+ }
70
+ /*
71
+ * 리비전은 **쓴 순서**다(일어난 순서가 아니다). 지난 사실이 지금 번호를 받는 것이 맞다 — 저널을
72
+ * 시각으로 읽는 쪽은 `eventTime` 을 보고, 리비전은 같은 시각을 가릴 때만 쓴다.
73
+ */
74
+ const start = twin_engine_js_1.TwinEngine.journalHead(domainId, instanceId);
75
+ const items = fresh.map((event, i) => ({ event, revision: start + i + 1 }));
76
+ await twin_engine_js_1.TwinEngine.persistCarried(domainId, instanceId, items);
77
+ twin_engine_js_1.TwinEngine.advanceJournalHead(domainId, instanceId, items.length);
78
+ /*
79
+ * 장부에 적는다 — **유입을 부르는 곳은 모두 적는다.** 한 경로만 빠지면 그 트윈의 유입 건강이
80
+ * 「모름」으로 보인다(§`ingest-health-wiring`).
81
+ *
82
+ * 건네받지 못한 수(`undelivered`)는 **거절한 것과 이미 있어서 건너뛴 것의 합**이다. 건너뛴 것은
83
+ * 버려진 것이 아니지만 이번에 새로 적히지도 않았다 — 「통과」로 세면 채운 양이 실제보다 커 보인다.
84
+ */
85
+ twin_engine_js_1.TwinEngine.recordIngestResult(domainId, instanceId, records.length, refused.map(r => ({ record: r.record, errors: [r.reason] })), nowMs, skipped);
86
+ return { written: items.length, skipped, refused };
87
+ }
88
+ function safeParse(text) {
89
+ try {
90
+ return JSON.parse(text);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ //# sourceMappingURL=backfill-period-facts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backfill-period-facts.js","sourceRoot":"","sources":["../../../server/service/twin-backfill/backfill-period-facts.ts"],"names":[],"mappings":"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","sourcesContent":["import { getRepository } from '@things-factory/shell'\n\nimport { TwinEvent } from '../twin-event/twin-event.js'\nimport { TwinEngine } from '../../engine/twin-engine.js'\nimport { ingestCanonicalRecords } from '../../engine/canonical-ingest.js'\nimport { absoluteTime } from '../../engine/read-time.js'\nimport { judgeBackfill, splitAlreadyThere } from './backfill-shape.js'\n\n/*\n * 지난 기록 채우기 — **저널에만 쓴다. 트윈의 「지금」을 건드리지 않는다.**\n *\n * ── 왜 라이브 문으로 넣을 수 없나 (2026-08-30) ──────────────────────────────\n * 커널의 상태는 「지금」이다. 늦게 온 옛 표본은 가드가 막는다(그렇게 만들었다). 지난 기록을 라이브\n * 문으로 넣으면 둘 중 하나다 — 가드에 막히거나, 트윈의 시계가 과거로 끌린다. 둘 다 나쁘다.\n *\n * 그래서 이 길은 저널에만 쓴다. 상태는 그대로 두고, 저널을 읽는 쪽(성과 계산·이력 화면)만 그 사실을\n * 보게 된다.\n *\n * ── 무엇을 받나 ────────────────────────────────────────────────────────────\n * **마감된 구간 사실만** 받는다. 시점의 관측(계량 표본·설비 상태·발전 적산)은 받지 않는다 — 그것들은\n * 커널의 상태를 바꾸는 사실이라, 상태를 건너뛰고 저널에만 넣으면 상태와 저널이 갈라진다.\n *\n * ── 같은 기간을 두 번 넣지 않는다 ──────────────────────────────────────────\n * 마감된 구간 사실은 정체성이 내용에 있다(커널 0.7.81: 종류·대상·시작·끝). 그 id 로 이미 있는 것을\n * 가려낸다. 저장소의 제약으로 막지 않는 이유는 드라이버가 다섯이고 충돌 처리가 제각각이기 때문이다 —\n * 구간 단위로 한 번 읽어 가려내는 편이 이식성이 있고 값도 싸다(한 시간에 한 줄이면 하루 24줄이다).\n *\n * ── 보존보다 오래된 것은 받지 않는다 ───────────────────────────────────────\n * 저널은 보존 기간이 지나면 지운다. 그보다 오래된 것을 채우면 다음 정리에서 지워진다. 조용히 넣고\n * 지워지면 아무도 모르므로, **거절하고 이유를 말한다.**\n */\n\nexport interface BackfillResult {\n /** 저널에 새로 적은 수. */\n written: number\n /** 이미 있어서 건너뛴 수 — 다시 읽어도 중복이 쌓이지 않는다는 뜻이다. */\n skipped: number\n /** 받지 않은 것과 그 이유. 조용히 버리지 않는다. */\n refused: { record: unknown; reason: string }[]\n}\n\n/** 이 도메인의 보존 기간(일). 정책이 없으면 `undefined` — 그때는 오래됨을 판정하지 않는다. */\nasync function retentionDaysOf(domainId: string): Promise<number | undefined> {\n const perDomain = TwinEngine.retentionDaysOf ? await TwinEngine.retentionDaysOf(domainId).catch(() => undefined) : undefined\n const days = perDomain ?? TwinEngine.JOURNAL_RETENTION_DAYS\n return days && days > 0 ? days : undefined\n}\n\n/**\n * 지난 기록을 저널에 채운다.\n *\n * `records` 는 라이브와 **같은 모양**이다 — 커넥터가 다른 어휘를 배울 필요가 없다. 검증·갈림·id 만들기는\n * 커널의 유입이 그대로 한다(§`ingestCanonicalRecords`).\n */\nexport async function backfillPeriodFacts(input: {\n domainId: string\n instanceId: string\n records: unknown[]\n /** 이 시각을 「지금」으로 본다 — 보존 판정의 기준. 시험이 시각을 정할 수 있게 받는다. */\n nowMs?: number\n}): Promise<BackfillResult> {\n const { domainId, instanceId, records } = input\n const nowMs = input.nowMs ?? Date.now()\n const refused: { record: unknown; reason: string }[] = []\n\n if (!Array.isArray(records) || !records.length) return { written: 0, skipped: 0, refused }\n\n /* 검증과 갈림은 라이브와 같은 것을 쓴다 — 두 벌이 되면 채운 사실과 관측한 사실이 다른 규칙을 지난다. */\n const { accepted, rejected } = ingestCanonicalRecords(\n records as any,\n domainId,\n new Date(nowMs).toISOString(),\n /* 어느 트윈의 사실인가 — 마감된 구간의 이름이 이것으로 갈린다(커널 §`SubjectBasis`). */\n TwinEngine.factScope(domainId, instanceId)\n )\n for (const r of rejected) refused.push({ record: r.record, reason: (r.errors ?? []).join(' · ') })\n\n const days = await retentionDaysOf(domainId)\n\n const judged = judgeBackfill(accepted as any[], { nowMs, retentionDays: days })\n refused.push(...judged.refused)\n const keep = judged.keep\n\n if (!keep.length) {\n TwinEngine.recordIngestResult(\n domainId,\n instanceId,\n records.length,\n refused.map(r => ({ record: r.record, errors: [r.reason] })),\n nowMs,\n 0\n )\n return { written: 0, skipped: 0, refused }\n }\n\n /*\n * 이미 있는 것을 가려낸다 — 그 구간의 그 종류만 읽는다(색인이 받쳐 준다). 페이로드의 id 로 견주고,\n * 저장소 제약에 기대지 않는다(드라이버 다섯).\n */\n const times = keep.map(e => absoluteTime(e.eventTime)!.getTime())\n const fromAt = new Date(Math.min(...times))\n const toAt = new Date(Math.max(...times))\n const types = [...new Set(keep.map(e => String(e.eventType)))]\n\n const existing = await getRepository(TwinEvent)\n .createQueryBuilder('e')\n .select(['e.payload AS \"payload\"'])\n .where('e.domain = :domainId', { domainId })\n .andWhere('e.instanceId = :instanceId', { instanceId })\n .andWhere('e.eventType IN (:...types)', { types })\n .andWhere('e.eventTime BETWEEN :fromAt AND :toAt', { fromAt, toAt })\n .getRawMany<{ payload: unknown }>()\n\n const seen = new Set<string>()\n for (const row of existing) {\n const p: any = typeof row.payload === 'string' ? safeParse(row.payload) : row.payload\n const id = p?.eventId\n if (typeof id === 'string' && id) seen.add(id)\n }\n\n const { fresh, skipped } = splitAlreadyThere(keep, seen)\n if (!fresh.length) {\n TwinEngine.recordIngestResult(\n domainId,\n instanceId,\n records.length,\n refused.map(r => ({ record: r.record, errors: [r.reason] })),\n nowMs,\n skipped\n )\n return { written: 0, skipped, refused }\n }\n\n /*\n * 리비전은 **쓴 순서**다(일어난 순서가 아니다). 지난 사실이 지금 번호를 받는 것이 맞다 — 저널을\n * 시각으로 읽는 쪽은 `eventTime` 을 보고, 리비전은 같은 시각을 가릴 때만 쓴다.\n */\n const start = TwinEngine.journalHead(domainId, instanceId)\n const items = fresh.map((event, i) => ({ event, revision: start + i + 1 }))\n\n await TwinEngine.persistCarried(domainId, instanceId, items)\n TwinEngine.advanceJournalHead(domainId, instanceId, items.length)\n\n /*\n * 장부에 적는다 — **유입을 부르는 곳은 모두 적는다.** 한 경로만 빠지면 그 트윈의 유입 건강이\n * 「모름」으로 보인다(§`ingest-health-wiring`).\n *\n * 건네받지 못한 수(`undelivered`)는 **거절한 것과 이미 있어서 건너뛴 것의 합**이다. 건너뛴 것은\n * 버려진 것이 아니지만 이번에 새로 적히지도 않았다 — 「통과」로 세면 채운 양이 실제보다 커 보인다.\n */\n TwinEngine.recordIngestResult(\n domainId,\n instanceId,\n records.length,\n refused.map(r => ({ record: r.record, errors: [r.reason] })),\n nowMs,\n skipped\n )\n return { written: items.length, skipped, refused }\n}\n\nfunction safeParse(text: string): unknown {\n try {\n return JSON.parse(text)\n } catch {\n return undefined\n }\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /** 이 길이 받는 사실 — 마감된 구간을 말하는 것들. */
2
+ export declare const PERIOD_FACTS: readonly string[];
3
+ export interface BackfillJudgement {
4
+ /** 저널에 넣어도 되는 것. */
5
+ keep: any[];
6
+ /** 받지 않은 것과 그 이유 — 조용히 버리지 않는다. */
7
+ refused: {
8
+ record: unknown;
9
+ reason: string;
10
+ }[];
11
+ }
12
+ /**
13
+ * 무엇을 채울 수 있나.
14
+ *
15
+ * 세 가지로 거절한다.
16
+ *
17
+ * 마감된 구간이 아니다 상태를 바꾸는 사실을 저널에만 넣으면 상태와 저널이 갈라진다
18
+ * 아직 오지 않은 시각 지난 기록이 아니다
19
+ * 보존보다 오래됐다 넣어도 다음 정리에서 지워진다 — 조용히 사라지느니 거절한다
20
+ */
21
+ export declare function judgeBackfill(envelopes: readonly any[], opts: {
22
+ nowMs: number;
23
+ retentionDays?: number;
24
+ }): BackfillJudgement;
25
+ /**
26
+ * 이미 있는 것을 가려낸다 — **정체성은 내용에 있다**(커널 0.7.81).
27
+ *
28
+ * 저장소의 제약으로 막지 않는 이유는 드라이버가 다섯이고 충돌 처리가 제각각이기 때문이다.
29
+ * 구간 단위로 한 번 읽어 가려내는 편이 이식성이 있고 값도 싸다.
30
+ */
31
+ export declare function splitAlreadyThere(keep: readonly any[], existingIds: ReadonlySet<string>): {
32
+ fresh: any[];
33
+ skipped: number;
34
+ };
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ /*
3
+ * 지난 기록 채우기의 **판단** — 저장소를 모르는 순수 함수.
4
+ *
5
+ * 무엇을 받고 무엇을 거절하는지가 이 파일에 다 있다. 저장소를 아는 코드(`backfill-period-facts`)는
6
+ * 이 판단을 부르고 쓰기만 한다. 갈라 두는 이유는 이 저장소가 이미 쓰는 방식이다 —
7
+ * 판단은 시험이 데이터베이스 없이 지킬 수 있어야 한다(§`event-type-split-shape`).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.PERIOD_FACTS = void 0;
11
+ exports.judgeBackfill = judgeBackfill;
12
+ exports.splitAlreadyThere = splitAlreadyThere;
13
+ /** 이 길이 받는 사실 — 마감된 구간을 말하는 것들. */
14
+ exports.PERIOD_FACTS = [
15
+ 'energy.usage.period',
16
+ 'energy.generation.period',
17
+ 'energy.generation.price',
18
+ 'energy.tariff.basis',
19
+ 'energy.bill'
20
+ ];
21
+ /**
22
+ * 무엇을 채울 수 있나.
23
+ *
24
+ * 세 가지로 거절한다.
25
+ *
26
+ * 마감된 구간이 아니다 상태를 바꾸는 사실을 저널에만 넣으면 상태와 저널이 갈라진다
27
+ * 아직 오지 않은 시각 지난 기록이 아니다
28
+ * 보존보다 오래됐다 넣어도 다음 정리에서 지워진다 — 조용히 사라지느니 거절한다
29
+ */
30
+ function judgeBackfill(envelopes, opts) {
31
+ const keep = [];
32
+ const refused = [];
33
+ const horizonMs = opts.retentionDays === undefined ? undefined : opts.nowMs - opts.retentionDays * 86_400_000;
34
+ for (const env of envelopes) {
35
+ const type = String(env?.eventType ?? '');
36
+ if (!exports.PERIOD_FACTS.includes(type)) {
37
+ refused.push({
38
+ record: env,
39
+ reason: `${type} 는 마감된 구간 사실이 아니다 — 이 길은 저널에만 쓰므로, 상태를 바꾸는 사실을 여기로 넣으면 ` +
40
+ '상태와 저널이 갈라진다. 라이브 문으로 보내라'
41
+ });
42
+ continue;
43
+ }
44
+ const atMs = Date.parse(String(env?.eventTime ?? ''));
45
+ if (!Number.isFinite(atMs)) {
46
+ refused.push({ record: env, reason: '사건 시각을 읽을 수 없다' });
47
+ continue;
48
+ }
49
+ if (atMs > opts.nowMs) {
50
+ refused.push({ record: env, reason: `아직 오지 않은 시각이다(${env?.eventTime}) — 지난 기록이 아니다` });
51
+ continue;
52
+ }
53
+ if (horizonMs !== undefined && atMs < horizonMs) {
54
+ refused.push({
55
+ record: env,
56
+ reason: `보존 기간(${opts.retentionDays}일)보다 오래됐다(${env?.eventTime}) — 넣어도 다음 정리에서 지워진다. ` +
57
+ '더 뒤까지 보려면 보존 기간을 늘려야 한다'
58
+ });
59
+ continue;
60
+ }
61
+ keep.push(env);
62
+ }
63
+ return { keep, refused };
64
+ }
65
+ /**
66
+ * 이미 있는 것을 가려낸다 — **정체성은 내용에 있다**(커널 0.7.81).
67
+ *
68
+ * 저장소의 제약으로 막지 않는 이유는 드라이버가 다섯이고 충돌 처리가 제각각이기 때문이다.
69
+ * 구간 단위로 한 번 읽어 가려내는 편이 이식성이 있고 값도 싸다.
70
+ */
71
+ function splitAlreadyThere(keep, existingIds) {
72
+ const fresh = keep.filter(e => typeof e?.eventId === 'string' && e.eventId && !existingIds.has(e.eventId));
73
+ return { fresh, skipped: keep.length - fresh.length };
74
+ }
75
+ //# sourceMappingURL=backfill-shape.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backfill-shape.js","sourceRoot":"","sources":["../../../server/service/twin-backfill/backfill-shape.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AA2BH,sCAyCC;AAQD,8CAGC;AA7ED,mCAAmC;AACtB,QAAA,YAAY,GAAsB;IAC7C,qBAAqB;IACrB,0BAA0B;IAC1B,yBAAyB;IACzB,qBAAqB;IACrB,aAAa;CACd,CAAA;AASD;;;;;;;;GAQG;AACH,SAAgB,aAAa,CAC3B,SAAyB,EACzB,IAA+C;IAE/C,MAAM,IAAI,GAAU,EAAE,CAAA;IACtB,MAAM,OAAO,GAA0C,EAAE,CAAA;IACzD,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,aAAa,GAAG,UAAU,CAAA;IAE7G,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC,CAAA;QACzC,IAAI,CAAC,oBAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC;gBACX,MAAM,EAAE,GAAG;gBACX,MAAM,EACJ,GAAG,IAAI,yDAAyD;oBAChE,2BAA2B;aAC9B,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,CAAA;QACrD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAA;YACvD,SAAQ;QACV,CAAC;QACD,IAAI,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACtB,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,iBAAiB,GAAG,EAAE,SAAS,gBAAgB,EAAE,CAAC,CAAA;YACtF,SAAQ;QACV,CAAC;QACD,IAAI,SAAS,KAAK,SAAS,IAAI,IAAI,GAAG,SAAS,EAAE,CAAC;YAChD,OAAO,CAAC,IAAI,CAAC;gBACX,MAAM,EAAE,GAAG;gBACX,MAAM,EACJ,SAAS,IAAI,CAAC,aAAa,aAAa,GAAG,EAAE,SAAS,wBAAwB;oBAC9E,yBAAyB;aAC5B,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAChB,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC1B,CAAC;AAED;;;;;GAKG;AACH,SAAgB,iBAAiB,CAAC,IAAoB,EAAE,WAAgC;IACtF,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAA;IAC1G,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAA;AACvD,CAAC","sourcesContent":["/*\n * 지난 기록 채우기의 **판단** — 저장소를 모르는 순수 함수.\n *\n * 무엇을 받고 무엇을 거절하는지가 이 파일에 다 있다. 저장소를 아는 코드(`backfill-period-facts`)는\n * 이 판단을 부르고 쓰기만 한다. 갈라 두는 이유는 이 저장소가 이미 쓰는 방식이다 —\n * 판단은 시험이 데이터베이스 없이 지킬 수 있어야 한다(§`event-type-split-shape`).\n */\n\n/** 이 길이 받는 사실 — 마감된 구간을 말하는 것들. */\nexport const PERIOD_FACTS: readonly string[] = [\n 'energy.usage.period',\n 'energy.generation.period',\n 'energy.generation.price',\n 'energy.tariff.basis',\n 'energy.bill'\n]\n\nexport interface BackfillJudgement {\n /** 저널에 넣어도 되는 것. */\n keep: any[]\n /** 받지 않은 것과 그 이유 — 조용히 버리지 않는다. */\n refused: { record: unknown; reason: string }[]\n}\n\n/**\n * 무엇을 채울 수 있나.\n *\n * 세 가지로 거절한다.\n *\n * 마감된 구간이 아니다 상태를 바꾸는 사실을 저널에만 넣으면 상태와 저널이 갈라진다\n * 아직 오지 않은 시각 지난 기록이 아니다\n * 보존보다 오래됐다 넣어도 다음 정리에서 지워진다 — 조용히 사라지느니 거절한다\n */\nexport function judgeBackfill(\n envelopes: readonly any[],\n opts: { nowMs: number; retentionDays?: number }\n): BackfillJudgement {\n const keep: any[] = []\n const refused: { record: unknown; reason: string }[] = []\n const horizonMs = opts.retentionDays === undefined ? undefined : opts.nowMs - opts.retentionDays * 86_400_000\n\n for (const env of envelopes) {\n const type = String(env?.eventType ?? '')\n if (!PERIOD_FACTS.includes(type)) {\n refused.push({\n record: env,\n reason:\n `${type} 는 마감된 구간 사실이 아니다 — 이 길은 저널에만 쓰므로, 상태를 바꾸는 사실을 여기로 넣으면 ` +\n '상태와 저널이 갈라진다. 라이브 문으로 보내라'\n })\n continue\n }\n const atMs = Date.parse(String(env?.eventTime ?? ''))\n if (!Number.isFinite(atMs)) {\n refused.push({ record: env, reason: '사건 시각을 읽을 수 없다' })\n continue\n }\n if (atMs > opts.nowMs) {\n refused.push({ record: env, reason: `아직 오지 않은 시각이다(${env?.eventTime}) — 지난 기록이 아니다` })\n continue\n }\n if (horizonMs !== undefined && atMs < horizonMs) {\n refused.push({\n record: env,\n reason:\n `보존 기간(${opts.retentionDays}일)보다 오래됐다(${env?.eventTime}) — 넣어도 다음 정리에서 지워진다. ` +\n '더 뒤까지 보려면 보존 기간을 늘려야 한다'\n })\n continue\n }\n keep.push(env)\n }\n\n return { keep, refused }\n}\n\n/**\n * 이미 있는 것을 가려낸다 — **정체성은 내용에 있다**(커널 0.7.81).\n *\n * 저장소의 제약으로 막지 않는 이유는 드라이버가 다섯이고 충돌 처리가 제각각이기 때문이다.\n * 구간 단위로 한 번 읽어 가려내는 편이 이식성이 있고 값도 싸다.\n */\nexport function splitAlreadyThere(keep: readonly any[], existingIds: ReadonlySet<string>): { fresh: any[]; skipped: number } {\n const fresh = keep.filter(e => typeof e?.eventId === 'string' && e.eventId && !existingIds.has(e.eventId))\n return { fresh, skipped: keep.length - fresh.length }\n}\n"]}
@@ -0,0 +1,2 @@
1
+ import { TwinBackfillResolver } from './twin-backfill-resolver.js';
2
+ export declare const resolvers: (typeof TwinBackfillResolver)[];
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolvers = void 0;
4
+ const twin_backfill_resolver_js_1 = require("./twin-backfill-resolver.js");
5
+ exports.resolvers = [twin_backfill_resolver_js_1.TwinBackfillResolver];
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../server/service/twin-backfill/index.ts"],"names":[],"mappings":";;;AAAA,2EAAkE;AAErD,QAAA,SAAS,GAAG,CAAC,gDAAoB,CAAC,CAAA","sourcesContent":["import { TwinBackfillResolver } from './twin-backfill-resolver.js'\n\nexport const resolvers = [TwinBackfillResolver]\n"]}
@@ -0,0 +1,10 @@
1
+ export declare class TwinBackfillResolver {
2
+ backfillTwinPeriodFacts(instanceId: string, records: unknown[], context: ResolverContext): Promise<{
3
+ written: number;
4
+ skipped: number;
5
+ refused: {
6
+ record: unknown;
7
+ reason: string;
8
+ }[];
9
+ }>;
10
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TwinBackfillResolver = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const type_graphql_1 = require("type-graphql");
6
+ const shell_1 = require("@things-factory/shell");
7
+ const backfill_period_facts_js_1 = require("./backfill-period-facts.js");
8
+ /*
9
+ * 지난 기록 채우기의 **문** — 커넥터가 트윈이 없던 동안의 날들을 뒤늦게 넣는다.
10
+ *
11
+ * ── 왜 문을 따로 두나 (2026-08-30) ────────────────────────────────────────
12
+ * 라이브 유입과 판단이 다르다. 라이브는 트윈의 「지금」을 바꾸고, 이쪽은 저널에만 쓴다. 같은 문으로
13
+ * 받으면 옛 표본이 가드에 막히거나 트윈의 시계가 과거로 끌린다(§`backfill-period-facts`).
14
+ *
15
+ * 레코드의 **모양은 라이브와 같다** — 커넥터가 다른 어휘를 배울 필요가 없다. 다른 것은 받는 종류뿐이고,
16
+ * 그 판단은 커널의 유입과 `judgeBackfill` 이 한다.
17
+ *
18
+ * ── 왜 이 파일이 늦게 생겼나 ──────────────────────────────────────────────
19
+ * 채우기 자체는 만들어 두고 **부르는 곳을 만들지 않아** 쓸 수 없는 채로 있었다. 8월 29일 발전량을
20
+ * 되살리려다 드러났다. 같은 부류를 그날 두 번 겪었다(커널의 발전 기간 문도 없었다).
21
+ */
22
+ let TwinBackfillResolver = class TwinBackfillResolver {
23
+ /* 트윈의 모든 변경과 같은 게이트다(ADR-0027) — 저널에 사실을 적는 일이므로 변경이다. */
24
+ async backfillTwinPeriodFacts(instanceId, records, context) {
25
+ return await (0, backfill_period_facts_js_1.backfillPeriodFacts)({ domainId: context.state.domain.id, instanceId, records });
26
+ }
27
+ };
28
+ exports.TwinBackfillResolver = TwinBackfillResolver;
29
+ tslib_1.__decorate([
30
+ (0, type_graphql_1.Directive)('@privilege(category: "twin", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
31
+ (0, type_graphql_1.Mutation)(returns => shell_1.ScalarObject, {
32
+ description: 'Backfill closed-period facts into a twin journal without touching its live state. Records use the same shape as live ingest; only closed-period facts are accepted (usage/generation periods, generation price, tariff basis, bill). Point-in-time observations are refused with a reason, as are times beyond journal retention. Re-running is safe: a period already present is skipped, because a closed period is identified by its content. Returns { written, skipped, refused }.'
33
+ }),
34
+ tslib_1.__param(0, (0, type_graphql_1.Arg)('instanceId')),
35
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('records', type => [shell_1.ScalarObject])),
36
+ tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
37
+ tslib_1.__metadata("design:type", Function),
38
+ tslib_1.__metadata("design:paramtypes", [String, Array, Object]),
39
+ tslib_1.__metadata("design:returntype", Promise)
40
+ ], TwinBackfillResolver.prototype, "backfillTwinPeriodFacts", null);
41
+ exports.TwinBackfillResolver = TwinBackfillResolver = tslib_1.__decorate([
42
+ (0, type_graphql_1.Resolver)()
43
+ ], TwinBackfillResolver);
44
+ //# sourceMappingURL=twin-backfill-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twin-backfill-resolver.js","sourceRoot":"","sources":["../../../server/service/twin-backfill/twin-backfill-resolver.ts"],"names":[],"mappings":";;;;AAAA,+CAAsE;AACtE,iDAAoD;AAEpD,yEAAgE;AAEhE;;;;;;;;;;;;;GAaG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAC/B,0DAA0D;IAMpD,AAAN,KAAK,CAAC,uBAAuB,CACR,UAAkB,EACG,OAAkB,EACnD,OAAwB;QAE/B,OAAO,MAAM,IAAA,8CAAmB,EAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAA;IAC9F,CAAC;CACF,CAAA;AAdY,oDAAoB;AAOzB;IALL,IAAA,wBAAS,EAAC,uGAAuG,CAAC;IAClH,IAAA,uBAAQ,EAAC,OAAO,CAAC,EAAE,CAAC,oBAAY,EAAE;QACjC,WAAW,EACT,ydAAyd;KAC5d,CAAC;IAEC,mBAAA,IAAA,kBAAG,EAAC,YAAY,CAAC,CAAA;IACjB,mBAAA,IAAA,kBAAG,EAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,oBAAY,CAAC,CAAC,CAAA;IACtC,mBAAA,IAAA,kBAAG,GAAE,CAAA;;;;mEAGP;+BAbU,oBAAoB;IADhC,IAAA,uBAAQ,GAAE;GACE,oBAAoB,CAchC","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'\nimport { ScalarObject } from '@things-factory/shell'\n\nimport { backfillPeriodFacts } from './backfill-period-facts.js'\n\n/*\n * 지난 기록 채우기의 **문** — 커넥터가 트윈이 없던 동안의 날들을 뒤늦게 넣는다.\n *\n * ── 왜 문을 따로 두나 (2026-08-30) ────────────────────────────────────────\n * 라이브 유입과 판단이 다르다. 라이브는 트윈의 「지금」을 바꾸고, 이쪽은 저널에만 쓴다. 같은 문으로\n * 받으면 옛 표본이 가드에 막히거나 트윈의 시계가 과거로 끌린다(§`backfill-period-facts`).\n *\n * 레코드의 **모양은 라이브와 같다** — 커넥터가 다른 어휘를 배울 필요가 없다. 다른 것은 받는 종류뿐이고,\n * 그 판단은 커널의 유입과 `judgeBackfill` 이 한다.\n *\n * ── 왜 이 파일이 늦게 생겼나 ──────────────────────────────────────────────\n * 채우기 자체는 만들어 두고 **부르는 곳을 만들지 않아** 쓸 수 없는 채로 있었다. 8월 29일 발전량을\n * 되살리려다 드러났다. 같은 부류를 그날 두 번 겪었다(커널의 발전 기간 문도 없었다).\n */\n@Resolver()\nexport class TwinBackfillResolver {\n /* 트윈의 모든 변경과 같은 게이트다(ADR-0027) — 저널에 사실을 적는 일이므로 변경이다. */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, {\n description:\n 'Backfill closed-period facts into a twin journal without touching its live state. Records use the same shape as live ingest; only closed-period facts are accepted (usage/generation periods, generation price, tariff basis, bill). Point-in-time observations are refused with a reason, as are times beyond journal retention. Re-running is safe: a period already present is skipped, because a closed period is identified by its content. Returns { written, skipped, refused }.'\n })\n async backfillTwinPeriodFacts(\n @Arg('instanceId') instanceId: string,\n @Arg('records', type => [ScalarObject]) records: unknown[],\n @Ctx() context: ResolverContext\n ): Promise<{ written: number; skipped: number; refused: { record: unknown; reason: string }[] }> {\n return await backfillPeriodFacts({ domainId: context.state.domain.id, instanceId, records })\n }\n}\n"]}
@@ -4,7 +4,7 @@ exports.TwinControlMutation = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const log_js_1 = require("../../engine/log.js");
6
6
  const type_graphql_1 = require("type-graphql");
7
- const twin_kernel_1 = require("@operato/twin-kernel");
7
+ const ops_contract_1 = require("@operato/ops-contract");
8
8
  const shell_1 = require("@things-factory/shell");
9
9
  const shell_2 = require("@things-factory/shell");
10
10
  const index_js_1 = require("../../engine/index.js");
@@ -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() });
@@ -81,7 +81,7 @@ let TwinControlMutation = class TwinControlMutation {
81
81
  * 들고 있다(계약의 주인) — 호스트가 따로 만들면 방언이 생긴다.
82
82
  */
83
83
  if (action === 'stimulus') {
84
- const v = (0, twin_kernel_1.validateScenario)(scenario);
84
+ const v = (0, ops_contract_1.validateScenario)(scenario);
85
85
  if (v.ok === false)
86
86
  return { ok: false, code: 'invalid', detail: v.errorCode, params: v.errorParams };
87
87
  }