@things-factory/headless-twin 10.0.8 → 10.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/dist-server/engine/attention-digest.d.ts +52 -0
  2. package/dist-server/engine/attention-digest.js +76 -0
  3. package/dist-server/engine/attention-digest.js.map +1 -0
  4. package/dist-server/engine/canonical-ingest.d.ts +2 -2
  5. package/dist-server/engine/canonical-ingest.js +24 -2
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.d.ts +33 -0
  8. package/dist-server/engine/command-routing.js +53 -0
  9. package/dist-server/engine/command-routing.js.map +1 -0
  10. package/dist-server/engine/energy-topology.d.ts +86 -0
  11. package/dist-server/engine/energy-topology.js +144 -0
  12. package/dist-server/engine/energy-topology.js.map +1 -0
  13. package/dist-server/engine/index.d.ts +11 -0
  14. package/dist-server/engine/index.js +17 -0
  15. package/dist-server/engine/index.js.map +1 -1
  16. package/dist-server/engine/kpi-baseline.d.ts +78 -0
  17. package/dist-server/engine/kpi-baseline.js +123 -0
  18. package/dist-server/engine/kpi-baseline.js.map +1 -0
  19. package/dist-server/engine/kpi-fold.d.ts +137 -1
  20. package/dist-server/engine/kpi-fold.js +225 -0
  21. package/dist-server/engine/kpi-fold.js.map +1 -1
  22. package/dist-server/engine/kpi-query.d.ts +32 -1
  23. package/dist-server/engine/kpi-query.js +262 -20
  24. package/dist-server/engine/kpi-query.js.map +1 -1
  25. package/dist-server/engine/kpi-target.d.ts +17 -0
  26. package/dist-server/engine/kpi-target.js +24 -2
  27. package/dist-server/engine/kpi-target.js.map +1 -1
  28. package/dist-server/engine/live-attentions.d.ts +1 -0
  29. package/dist-server/engine/live-attentions.js +7 -1
  30. package/dist-server/engine/live-attentions.js.map +1 -1
  31. package/dist-server/engine/live-feed-registry.d.ts +25 -0
  32. package/dist-server/engine/live-feed-registry.js +51 -0
  33. package/dist-server/engine/live-feed-registry.js.map +1 -0
  34. package/dist-server/engine/load-meter.d.ts +181 -0
  35. package/dist-server/engine/load-meter.js +267 -0
  36. package/dist-server/engine/load-meter.js.map +1 -0
  37. package/dist-server/engine/local-declarations.d.ts +262 -0
  38. package/dist-server/engine/local-declarations.js +528 -0
  39. package/dist-server/engine/local-declarations.js.map +1 -0
  40. package/dist-server/engine/model-basis.d.ts +39 -6
  41. package/dist-server/engine/model-basis.js +69 -9
  42. package/dist-server/engine/model-basis.js.map +1 -1
  43. package/dist-server/engine/model-vocabulary.d.ts +17 -0
  44. package/dist-server/engine/model-vocabulary.js +81 -0
  45. package/dist-server/engine/model-vocabulary.js.map +1 -0
  46. package/dist-server/engine/oee-accumulator.d.ts +28 -0
  47. package/dist-server/engine/oee-accumulator.js +26 -1
  48. package/dist-server/engine/oee-accumulator.js.map +1 -1
  49. package/dist-server/engine/operation-basis.d.ts +32 -0
  50. package/dist-server/engine/operation-basis.js +87 -0
  51. package/dist-server/engine/operation-basis.js.map +1 -0
  52. package/dist-server/engine/property-effects.d.ts +30 -0
  53. package/dist-server/engine/property-effects.js +192 -0
  54. package/dist-server/engine/property-effects.js.map +1 -0
  55. package/dist-server/engine/runtime-key.d.ts +15 -0
  56. package/dist-server/engine/runtime-key.js +64 -0
  57. package/dist-server/engine/runtime-key.js.map +1 -0
  58. package/dist-server/engine/spec-coverage.js +3 -3
  59. package/dist-server/engine/spec-coverage.js.map +1 -1
  60. package/dist-server/engine/state-axes.d.ts +21 -0
  61. package/dist-server/engine/state-axes.js +59 -0
  62. package/dist-server/engine/state-axes.js.map +1 -0
  63. package/dist-server/engine/structure-diff.js +1 -1
  64. package/dist-server/engine/structure-diff.js.map +1 -1
  65. package/dist-server/engine/travel-estimator.d.ts +9 -2
  66. package/dist-server/engine/travel-estimator.js +11 -6
  67. package/dist-server/engine/travel-estimator.js.map +1 -1
  68. package/dist-server/engine/twin-engine.d.ts +418 -57
  69. package/dist-server/engine/twin-engine.js +1599 -306
  70. package/dist-server/engine/twin-engine.js.map +1 -1
  71. package/dist-server/engine/warm-start.d.ts +93 -13
  72. package/dist-server/engine/warm-start.js +114 -14
  73. package/dist-server/engine/warm-start.js.map +1 -1
  74. package/dist-server/index.d.ts +1 -0
  75. package/dist-server/index.js +17 -11
  76. package/dist-server/index.js.map +1 -1
  77. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.d.ts +5 -0
  78. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +48 -0
  79. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -0
  80. package/dist-server/migrations/index.d.ts +2 -0
  81. package/dist-server/migrations/index.js +10 -0
  82. package/dist-server/migrations/index.js.map +1 -0
  83. package/dist-server/service/index.d.ts +3 -2
  84. package/dist-server/service/index.js +25 -17
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/discovery-result.d.ts +34 -0
  87. package/dist-server/service/reference/discovery-result.js +84 -0
  88. package/dist-server/service/reference/discovery-result.js.map +1 -0
  89. package/dist-server/service/reference/ingest-space.d.ts +53 -0
  90. package/dist-server/service/reference/ingest-space.js +79 -0
  91. package/dist-server/service/reference/ingest-space.js.map +1 -0
  92. package/dist-server/service/reference/knob-defaults.d.ts +20 -0
  93. package/dist-server/service/reference/knob-defaults.js +59 -0
  94. package/dist-server/service/reference/knob-defaults.js.map +1 -0
  95. package/dist-server/service/reference/reference-live.d.ts +23 -0
  96. package/dist-server/service/reference/reference-live.js +123 -6
  97. package/dist-server/service/reference/reference-live.js.map +1 -1
  98. package/dist-server/service/reference/reference-master.d.ts +227 -11
  99. package/dist-server/service/reference/reference-master.js +206 -25
  100. package/dist-server/service/reference/reference-master.js.map +1 -1
  101. package/dist-server/service/reference/reference-resolver.d.ts +33 -3
  102. package/dist-server/service/reference/reference-resolver.js +290 -23
  103. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  104. package/dist-server/service/reference/template-registry.d.ts +19 -3
  105. package/dist-server/service/reference/template-registry.js.map +1 -1
  106. package/dist-server/service/twin-attention/twin-attention-query.d.ts +8 -1
  107. package/dist-server/service/twin-attention/twin-attention-query.js +39 -8
  108. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  109. package/dist-server/service/twin-audit/command-audit.d.ts +37 -0
  110. package/dist-server/service/twin-audit/command-audit.js +53 -0
  111. package/dist-server/service/twin-audit/command-audit.js.map +1 -0
  112. package/dist-server/service/twin-audit/index.d.ts +4 -0
  113. package/dist-server/service/twin-audit/index.js +8 -0
  114. package/dist-server/service/twin-audit/index.js.map +1 -0
  115. package/dist-server/service/twin-audit/twin-audit-event.d.ts +24 -0
  116. package/dist-server/service/twin-audit/twin-audit-event.js +125 -0
  117. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -0
  118. package/dist-server/service/twin-audit/twin-audit-query.d.ts +4 -0
  119. package/dist-server/service/twin-audit/twin-audit-query.js +76 -0
  120. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -0
  121. package/dist-server/service/twin-control/twin-control-mutation.d.ts +2 -0
  122. package/dist-server/service/twin-control/twin-control-mutation.js +50 -11
  123. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  124. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  125. package/dist-server/service/twin-event/twin-event-keys.js +1 -1
  126. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  127. package/dist-server/service/twin-event/twin-event.d.ts +14 -5
  128. package/dist-server/service/twin-event/twin-event.js +46 -14
  129. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  130. package/dist-server/service/twin-forecast/forecast-metrics.d.ts +13 -0
  131. package/dist-server/service/twin-forecast/forecast-metrics.js +57 -0
  132. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -0
  133. package/dist-server/service/twin-forecast/forecast-tuning.d.ts +12 -0
  134. package/dist-server/service/twin-forecast/forecast-tuning.js +44 -0
  135. package/dist-server/service/twin-forecast/forecast-tuning.js.map +1 -0
  136. package/dist-server/service/twin-forecast/gap-analytics.d.ts +28 -2
  137. package/dist-server/service/twin-forecast/gap-analytics.js +43 -17
  138. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  139. package/dist-server/service/twin-forecast/twin-forecast-query.js +219 -39
  140. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  141. package/dist-server/service/twin-instance/twin-instance.d.ts +38 -5
  142. package/dist-server/service/twin-instance/twin-instance.js +66 -18
  143. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  144. package/dist-server/service/twin-journal/twin-journal-query.d.ts +11 -4
  145. package/dist-server/service/twin-journal/twin-journal-query.js +95 -15
  146. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  147. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +11 -5
  148. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +46 -27
  149. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  150. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  151. package/dist-server/service/twin-metrics/twin-metrics-query.js +63 -1
  152. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  153. package/dist-server/service/twin-model/epcis-coverage.d.ts +14 -0
  154. package/dist-server/service/twin-model/epcis-coverage.js +174 -0
  155. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -0
  156. package/dist-server/service/twin-model/iec61850-coverage.d.ts +14 -0
  157. package/dist-server/service/twin-model/iec61850-coverage.js +98 -0
  158. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -0
  159. package/dist-server/service/twin-model/index.d.ts +13 -0
  160. package/dist-server/service/twin-model/index.js +29 -0
  161. package/dist-server/service/twin-model/index.js.map +1 -0
  162. package/dist-server/service/twin-model/isa95-coverage.d.ts +48 -0
  163. package/dist-server/service/twin-model/isa95-coverage.js +155 -0
  164. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -0
  165. package/dist-server/service/twin-model/project-structure.d.ts +16 -0
  166. package/dist-server/service/twin-model/project-structure.js +173 -0
  167. package/dist-server/service/twin-model/project-structure.js.map +1 -0
  168. package/dist-server/service/twin-model/standard-coverage.d.ts +9 -0
  169. package/dist-server/service/twin-model/standard-coverage.js +37 -0
  170. package/dist-server/service/twin-model/standard-coverage.js.map +1 -0
  171. package/dist-server/service/twin-model/twin-equipment.d.ts +64 -0
  172. package/dist-server/service/twin-model/twin-equipment.js +134 -0
  173. package/dist-server/service/twin-model/twin-equipment.js.map +1 -0
  174. package/dist-server/service/twin-model/twin-lineage-query.d.ts +3 -0
  175. package/dist-server/service/twin-model/twin-lineage-query.js +206 -0
  176. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -0
  177. package/dist-server/service/twin-model/twin-location.d.ts +58 -0
  178. package/dist-server/service/twin-model/twin-location.js +130 -0
  179. package/dist-server/service/twin-model/twin-location.js.map +1 -0
  180. package/dist-server/service/twin-model/twin-model-item-query.d.ts +141 -0
  181. package/dist-server/service/twin-model/twin-model-item-query.js +852 -0
  182. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -0
  183. package/dist-server/service/twin-model/twin-model-mutation.d.ts +57 -0
  184. package/dist-server/service/twin-model/twin-model-mutation.js +490 -0
  185. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -0
  186. package/dist-server/service/twin-model/twin-model-query.d.ts +43 -0
  187. package/dist-server/service/twin-model/twin-model-query.js +551 -0
  188. package/dist-server/service/twin-model/twin-model-query.js.map +1 -0
  189. package/dist-server/service/twin-model/twin-model-tree-query.d.ts +3 -0
  190. package/dist-server/service/twin-model/twin-model-tree-query.js +158 -0
  191. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -0
  192. package/dist-server/service/twin-model/twin-operation.d.ts +61 -0
  193. package/dist-server/service/twin-model/twin-operation.js +137 -0
  194. package/dist-server/service/twin-model/twin-operation.js.map +1 -0
  195. package/dist-server/service/twin-space/move-space.d.ts +65 -0
  196. package/dist-server/service/twin-space/move-space.js +166 -0
  197. package/dist-server/service/twin-space/move-space.js.map +1 -0
  198. package/dist-server/service/twin-space/space-integrity.d.ts +97 -0
  199. package/dist-server/service/twin-space/space-integrity.js +182 -0
  200. package/dist-server/service/twin-space/space-integrity.js.map +1 -0
  201. package/dist-server/service/twin-space/twin-space-area.js +4 -4
  202. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  203. package/dist-server/service/twin-space/twin-space-representation.js +6 -6
  204. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +63 -1
  206. package/dist-server/service/twin-space/twin-space-resolver.js +365 -18
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-server/service/twin-space/twin-space.d.ts +17 -1
  209. package/dist-server/service/twin-space/twin-space.js +15 -5
  210. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  211. package/dist-server/service/twin-state/twin-state-subscription.js +1 -1
  212. package/dist-server/service/twin-state/twin-state-subscription.js.map +1 -1
  213. package/dist-server/service/twin-structure/twin-structure.d.ts +1 -1
  214. package/dist-server/service/twin-structure/twin-structure.js +8 -7
  215. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  216. package/dist-server/service/twin-target/twin-target-resolver.js +21 -4
  217. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  218. package/dist-shared/axis-read.d.ts +12 -0
  219. package/dist-shared/axis-read.js +32 -0
  220. package/dist-shared/axis-read.js.map +1 -0
  221. package/dist-shared/entity-delta.d.ts +29 -0
  222. package/dist-shared/entity-delta.js +287 -0
  223. package/dist-shared/entity-delta.js.map +1 -0
  224. package/dist-shared/kpi-broadcast.d.ts +4 -0
  225. package/dist-shared/kpi-broadcast.js +16 -0
  226. package/dist-shared/kpi-broadcast.js.map +1 -0
  227. package/dist-shared/twin-level.d.ts +23 -0
  228. package/dist-shared/twin-level.js +52 -0
  229. package/dist-shared/twin-level.js.map +1 -0
  230. package/package.json +14 -12
  231. package/server/engine/attention-digest.ts +102 -0
  232. package/server/engine/canonical-ingest.ts +36 -4
  233. package/server/engine/command-routing.ts +67 -0
  234. package/server/engine/energy-topology.ts +189 -0
  235. package/server/engine/index.ts +17 -0
  236. package/server/engine/kpi-baseline.ts +202 -0
  237. package/server/engine/kpi-fold.ts +389 -1
  238. package/server/engine/kpi-query.ts +295 -22
  239. package/server/engine/kpi-target.ts +24 -2
  240. package/server/engine/live-attentions.ts +7 -2
  241. package/server/engine/live-feed-registry.ts +58 -0
  242. package/server/engine/load-meter.ts +384 -0
  243. package/server/engine/local-declarations.ts +700 -0
  244. package/server/engine/model-basis.ts +78 -10
  245. package/server/engine/model-vocabulary.ts +82 -0
  246. package/server/engine/oee-accumulator.ts +34 -1
  247. package/server/engine/operation-basis.ts +100 -0
  248. package/server/engine/property-effects.ts +199 -0
  249. package/server/engine/runtime-key.ts +58 -0
  250. package/server/engine/spec-coverage.ts +3 -3
  251. package/server/engine/state-axes.ts +60 -0
  252. package/server/engine/structure-diff.ts +1 -1
  253. package/server/engine/travel-estimator.ts +11 -6
  254. package/server/engine/twin-engine.ts +1710 -297
  255. package/server/engine/warm-start.ts +207 -22
  256. package/server/index.ts +13 -10
  257. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +43 -0
  258. package/server/migrations/index.ts +7 -0
  259. package/server/service/index.ts +9 -1
  260. package/server/service/reference/discovery-result.ts +95 -0
  261. package/server/service/reference/ingest-space.ts +104 -0
  262. package/server/service/reference/knob-defaults.ts +59 -0
  263. package/server/service/reference/reference-live.ts +121 -6
  264. package/server/service/reference/reference-master.ts +362 -35
  265. package/server/service/reference/reference-resolver.ts +307 -23
  266. package/server/service/reference/template-registry.ts +20 -5
  267. package/server/service/twin-attention/twin-attention-query.ts +43 -6
  268. package/server/service/twin-audit/command-audit.ts +81 -0
  269. package/server/service/twin-audit/index.ts +5 -0
  270. package/server/service/twin-audit/twin-audit-event.ts +112 -0
  271. package/server/service/twin-audit/twin-audit-query.ts +72 -0
  272. package/server/service/twin-control/twin-control-mutation.ts +53 -13
  273. package/server/service/twin-event/twin-event-keys.ts +1 -1
  274. package/server/service/twin-event/twin-event.ts +52 -15
  275. package/server/service/twin-forecast/forecast-metrics.ts +60 -0
  276. package/server/service/twin-forecast/forecast-tuning.ts +40 -0
  277. package/server/service/twin-forecast/gap-analytics.ts +48 -9
  278. package/server/service/twin-forecast/twin-forecast-query.ts +211 -38
  279. package/server/service/twin-instance/twin-instance.ts +127 -22
  280. package/server/service/twin-journal/twin-journal-query.ts +108 -14
  281. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +40 -23
  282. package/server/service/twin-metrics/twin-metrics-query.ts +60 -3
  283. package/server/service/twin-model/epcis-coverage.ts +195 -0
  284. package/server/service/twin-model/iec61850-coverage.ts +117 -0
  285. package/server/service/twin-model/index.ts +25 -0
  286. package/server/service/twin-model/isa95-coverage.ts +190 -0
  287. package/server/service/twin-model/project-structure.ts +199 -0
  288. package/server/service/twin-model/standard-coverage.ts +35 -0
  289. package/server/service/twin-model/twin-equipment.ts +150 -0
  290. package/server/service/twin-model/twin-lineage-query.ts +193 -0
  291. package/server/service/twin-model/twin-location.ts +140 -0
  292. package/server/service/twin-model/twin-model-item-query.ts +848 -0
  293. package/server/service/twin-model/twin-model-mutation.ts +503 -0
  294. package/server/service/twin-model/twin-model-query.ts +537 -0
  295. package/server/service/twin-model/twin-model-tree-query.ts +180 -0
  296. package/server/service/twin-model/twin-operation.ts +150 -0
  297. package/server/service/twin-space/move-space.ts +262 -0
  298. package/server/service/twin-space/space-integrity.ts +293 -0
  299. package/server/service/twin-space/twin-space-area.ts +4 -4
  300. package/server/service/twin-space/twin-space-representation.ts +6 -6
  301. package/server/service/twin-space/twin-space-resolver.ts +365 -22
  302. package/server/service/twin-space/twin-space.ts +40 -6
  303. package/server/service/twin-state/twin-state-subscription.ts +1 -1
  304. package/server/service/twin-structure/twin-structure.ts +10 -7
  305. package/server/service/twin-target/twin-target-resolver.ts +22 -4
  306. package/shared/axis-read.ts +28 -0
  307. package/shared/entity-delta.ts +286 -0
  308. package/shared/kpi-broadcast.ts +13 -0
  309. package/shared/twin-level.ts +48 -0
  310. package/test/adopt-structure-live.test.ts +133 -0
  311. package/test/attention-digest.test.ts +135 -0
  312. package/test/axis-read.test.ts +87 -0
  313. package/test/boot-resume.test.ts +205 -0
  314. package/test/canonical-ingest-vocabularies.test.ts +118 -0
  315. package/test/capability-mapping.test.ts +5 -5
  316. package/test/command-routing.test.ts +61 -0
  317. package/test/declaration-reaches-model.test.ts +178 -0
  318. package/test/discovery-result.test.ts +75 -0
  319. package/test/energy-topology.test.ts +113 -0
  320. package/test/entity-delta.test.ts +195 -25
  321. package/test/event-time-column.test.ts +67 -0
  322. package/test/forecast-metrics.test.ts +71 -0
  323. package/test/forecast-tuning.test.ts +42 -0
  324. package/test/gap-analytics.test.ts +34 -7
  325. package/test/ingest-bench.test.ts +9 -9
  326. package/test/ingest-running-guard.test.ts +136 -0
  327. package/test/ingest-space.test.ts +78 -0
  328. package/test/instance-cache-lifecycle.test.ts +110 -0
  329. package/test/kernel-kind-guard.test.ts +96 -0
  330. package/test/knob-defaults.test.ts +72 -0
  331. package/test/kpi-baseline-db.test.ts +216 -0
  332. package/test/kpi-baseline.test.ts +196 -0
  333. package/test/kpi-fold.test.ts +393 -2
  334. package/test/kpi-query-bench.test.ts +130 -0
  335. package/test/lineage-survives-restart.test.ts +203 -0
  336. package/test/live-feed-registry.test.ts +67 -0
  337. package/test/live-kernel-facts.test.ts +161 -0
  338. package/test/live-mirror-parity.test.ts +54 -14
  339. package/test/load-meter.test.ts +314 -0
  340. package/test/local-declarations.test.ts +554 -0
  341. package/test/master-to-twin.test.ts +128 -36
  342. package/test/model-basis.test.ts +56 -1
  343. package/test/model-vocabulary.test.ts +114 -0
  344. package/test/move-space.test.ts +149 -0
  345. package/test/mutation-gate.test.ts +122 -0
  346. package/test/oee-accumulator.test.ts +90 -7
  347. package/test/operation-basis.test.ts +100 -0
  348. package/test/operations-capability-db.test.ts +165 -0
  349. package/test/project-structure-db.test.ts +226 -0
  350. package/test/projection-reaches-screen.test.ts +272 -0
  351. package/test/property-effects.test.ts +96 -0
  352. package/test/registry-key-guard.test.ts +80 -0
  353. package/test/resync-origin-site.test.ts +77 -0
  354. package/test/runtime-key.test.ts +66 -0
  355. package/test/scale-twin-bench.test.ts +7 -7
  356. package/test/snapshot-freshness.test.ts +60 -0
  357. package/test/space-integrity.test.ts +223 -0
  358. package/test/standard-coverage.test.ts +94 -0
  359. package/test/state-axes.test.ts +74 -0
  360. package/test/streamline-e2e.test.ts +16 -16
  361. package/test/structure-revision-db.test.ts +16 -15
  362. package/test/tenant-registry-db.test.ts +149 -0
  363. package/test/twin-audit.test.ts +83 -0
  364. package/test/twin-model-item-db.test.ts +275 -0
  365. package/test/twin-model-tree-db.test.ts +176 -0
  366. package/test/twin-origin-resync.test.ts +114 -0
  367. package/test/warm-start-seam.test.ts +140 -0
  368. package/test/warm-start.test.ts +223 -4
  369. package/tsconfig.json +6 -1
  370. package/tsconfig.shared.json +23 -0
  371. package/tsconfig.shared.tsbuildinfo +1 -0
  372. package/tsconfig.tsbuildinfo +1 -0
  373. package/dist-server/engine/entity-delta.d.ts +0 -19
  374. package/dist-server/engine/entity-delta.js +0 -161
  375. package/dist-server/engine/entity-delta.js.map +0 -1
  376. package/dist-server/service/twin-event/backfill-keys.d.ts +0 -11
  377. package/dist-server/service/twin-event/backfill-keys.js +0 -63
  378. package/dist-server/service/twin-event/backfill-keys.js.map +0 -1
  379. package/dist-server/tsconfig.tsbuildinfo +0 -1
  380. package/server/engine/entity-delta.ts +0 -169
  381. package/server/service/twin-event/backfill-keys.ts +0 -72
@@ -0,0 +1,181 @@
1
+ /** 작업 이름 — 새 작업을 재기 시작하면 여기 늘린다(문자열을 자유롭게 쓰지 않는다: 오타가 새 칸이 된다). */
2
+ export declare const LOAD_PHASES: readonly ["tick", "snapshot", "deltas", "publish", "journal", "ingest", "forecast", "forkTick"];
3
+ export type LoadPhase = (typeof LOAD_PHASES)[number];
4
+ export interface PhaseStats {
5
+ /** 잰 횟수. */
6
+ count: number;
7
+ /** 마지막 소요(ms). */
8
+ lastMs: number;
9
+ /** 가장 오래 걸린 것(ms) — 평균은 순간 정체를 가린다. */
10
+ maxMs: number;
11
+ /** 소요 합(ms). 나눗셈은 읽는 쪽에서 한다. */
12
+ totalMs: number;
13
+ }
14
+ /** 창(window) 하나의 누적 — "지금 얼마나 쓰고 있나" 를 답한다. */
15
+ interface Window {
16
+ /** 창이 열린 시각. **아직 안 열렸으면 `-1`** — 0 으로 두면 시각 0 과 구별이 안 된다(검사가 잡았다). */
17
+ startMs: number;
18
+ phases: Partial<Record<LoadPhase, PhaseStats>>;
19
+ }
20
+ export interface LoadMeter {
21
+ /** **누적** — 프로세스가 뜬 뒤 전부. 총량과 최악값을 본다. */
22
+ phases: Partial<Record<LoadPhase, PhaseStats>>;
23
+ /** 지금 쌓고 있는 창. */
24
+ window: Window;
25
+ /** **마지막으로 닫힌 창** — 이것이 "실시간" 이다. 없으면 아직 한 창도 안 지났다. */
26
+ recent: (Window & {
27
+ endMs: number;
28
+ }) | null;
29
+ /** 이 트윈에서 만들어진 fork 수(누적) — 예측 한 번이 수십 개를 만든다. */
30
+ forksCreated: number;
31
+ /** 한 주기(틱)가 예산을 넘긴 횟수. */
32
+ overBudget: number;
33
+ /**
34
+ * 마지막 경고 시각(ms). **한 번도 안 했으면 `-Infinity`** — 0 으로 두면 "0 시점에 경고했다" 가 되어
35
+ * 첫 느린 틱이 조용히 지나간다(검사가 그것을 잡았다).
36
+ */
37
+ lastWarnMs: number;
38
+ }
39
+ export declare function newLoadMeter(): LoadMeter;
40
+ /**
41
+ * 예산 — 틱 간격의 절반. 넘으면 루프의 절반을 이 트윈 하나가 쓰고 있다는 뜻이다.
42
+ *
43
+ * 절반인 이유: 트윈이 여럿이라 각자 간격을 다 쓰면 이미 포화다. 절반에서 알아야 손 쓸 틈이 있다.
44
+ */
45
+ export declare const TICK_BUDGET_RATIO = 0.5;
46
+ /** 경고를 다시 낼 때까지의 최소 간격(ms) — 느린 트윈은 매 틱 느리므로, 그대로 두면 로그가 원인을 덮는다. */
47
+ export declare const WARN_COOLDOWN_MS = 30000;
48
+ /**
49
+ * 한 작업의 소요를 기록한다. **누적만 하고 판단하지 않는다.**
50
+ *
51
+ * 음수·NaN 은 기록하지 않는다(시계가 뒤로 간 경우). 없는 값을 0 으로 세면 평균이 거짓이 된다.
52
+ */
53
+ export declare function recordPhase(meter: LoadMeter, phase: LoadPhase, tookMs: number, nowMs?: number): void;
54
+ /** fork 하나가 만들어졌다 — 예측 한 번이 수십 개를 만든다. 수를 알아야 부하의 출처를 짚는다. */
55
+ export declare function recordFork(meter: LoadMeter, count?: number): void;
56
+ /** 실시간 창의 길이(ms) — 이보다 길게 보려면 누적을 본다. */
57
+ export declare const WINDOW_MS = 10000;
58
+ /** 창이 다 찼으면 닫고 새로 연다. **닫힌 창만 "최근" 으로 읽는다** — 채우는 중인 창은 과소 집계다. */
59
+ export declare function rollWindow(meter: LoadMeter, nowMs: number, windowMs?: number): void;
60
+ /**
61
+ * 이 주기가 예산을 넘었나 — 넘었으면 세고, **말할 때인지**까지 답한다.
62
+ */
63
+ export declare function judgeCycle(meter: LoadMeter, tookMs: number, tickMs: number, nowMs: number, budgetRatio?: number, cooldownMs?: number): {
64
+ over: boolean;
65
+ warn: boolean;
66
+ budgetMs: number;
67
+ };
68
+ export interface PhaseRow {
69
+ phase: LoadPhase;
70
+ count: number;
71
+ lastMs: number;
72
+ maxMs: number;
73
+ avgMs: number;
74
+ totalMs: number;
75
+ /** 잰 작업 전체 시간 중 이 작업의 몫(0~1). 어디를 고칠지 이 값이 정한다. */
76
+ share: number;
77
+ }
78
+ export interface LoadView {
79
+ phases: PhaseRow[];
80
+ measuredTotalMs: number;
81
+ /** 이 구간의 길이(ms). 누적은 프로세스 기동 이후라 알 수 없으므로 `null`. */
82
+ spanMs: number | null;
83
+ /** 구간 중 몇 %를 이 트윈이 썼나(0~1+). 구간을 모르면 `null` — **0 이 아니다.** */
84
+ busyRatio: number | null;
85
+ }
86
+ /**
87
+ * 읽기용 요약 — **실시간과 누적을 함께** 낸다.
88
+ *
89
+ * · `recent` — 마지막으로 **닫힌** 창. 지금 이 트윈이 루프를 얼마나 쓰고 있나. 채우는 중인 창은
90
+ * 과소 집계라 쓰지 않는다.
91
+ * · `total` — 기동 이후 전부. 총량과 최악값.
92
+ *
93
+ * 잰 적이 없으면 **`null` 이다.** 0 으로 채우면 "0ms 다"(= 아주 빠르다)로 읽혀, 재지 않은 것과 정반대의
94
+ * 뜻이 된다. 몫(`share`)도 잰 작업들 사이의 비율일 뿐 — **재지 않은 작업은 분모에 없다.**
95
+ */
96
+ export declare function loadSummary(meter: LoadMeter | undefined, tickMs: number): {
97
+ recent: LoadView | null;
98
+ total: LoadView | null;
99
+ forksCreated: number;
100
+ overBudget: number;
101
+ budgetMs: number;
102
+ /** 마지막 틱이 간격을 얼마나 썼나(0~1+). 1 이면 간격을 통째로 쓴다. */
103
+ loadRatio: number | null;
104
+ } | null;
105
+ /**
106
+ * 경고 한 줄 — **무엇이 느린지까지 말한다.** "틱이 800ms" 만으로는 어디를 볼지 알 수 없다.
107
+ *
108
+ * 가장 무거운 작업 셋을 몫과 함께 적는다.
109
+ */
110
+ export declare function slowTickMessage(instanceId: string, tookMs: number, budgetMs: number, meter: LoadMeter): string;
111
+ /**
112
+ * 전체 한눈에 — **누가 루프를 점유하고 있나.**
113
+ *
114
+ * 인스턴스 목록을 받아 대수·모드별 수·fork 총수를 세고, **최근 창의 부하가 큰 순서**로 줄을 세운다.
115
+ * 누적이 아니라 최근으로 세우는 이유: 지금 아픈 것을 먼저 봐야 한다(누적 1위는 오래 떠 있던 것일 뿐일
116
+ * 수 있다).
117
+ */
118
+ export declare function fleetLoad(rows: {
119
+ instanceId: string;
120
+ mode: string;
121
+ domainId?: string;
122
+ domainLabel?: string;
123
+ meter?: LoadMeter;
124
+ }[], tickMs: number): {
125
+ instances: number;
126
+ byMode: Record<string, number>;
127
+ /**
128
+ * **테넌트별 몫** — 운영자가 먼저 묻는 것은 "어느 테넌트가 루프를 먹나" 다.
129
+ *
130
+ * 실측으로 필요해졌다: 한 도메인 트윈 22개가 CPU 를 110% 쓰는 동안 다른 도메인 화면은 0 이었다.
131
+ * 무거운 순으로 낸다. 몫은 **잰 트윈들 사이의 비율**이다(안 잰 트윈은 분모에 없다).
132
+ */
133
+ byDomain: {
134
+ domainId: string;
135
+ label: string;
136
+ instances: number;
137
+ recentBusyMs: number;
138
+ share: number;
139
+ }[];
140
+ forksCreated: number;
141
+ /** 최근 창에서 트윈들이 쓴 시간의 합(ms) — 창 길이를 넘으면 루프가 이미 밀리고 있다. */
142
+ recentBusyMs: number;
143
+ /** 그 창의 길이(ms). 창이 하나도 안 닫혔으면 `null`. */
144
+ recentSpanMs: number | null;
145
+ /** 전체가 루프를 얼마나 쓰나(0~1+). 1 을 넘으면 **한 틱 안에 다 못 끝낸다.** */
146
+ fleetBusyRatio: number | null;
147
+ slowest: {
148
+ instanceId: string;
149
+ mode: string;
150
+ domainId?: string;
151
+ domainLabel?: string;
152
+ recentBusyMs: number;
153
+ topPhase: LoadPhase | null;
154
+ overBudget: number;
155
+ }[];
156
+ };
157
+ /**
158
+ * 프로세스가 실제로 쓴 CPU — **계기판의 다른 숫자들과 뜻이 다르다.**
159
+ *
160
+ * 이 파일의 다른 값들은 트윈의 **계측된 8개 작업**만 잰 것이다. 그런데 프로세스는 그 밖에도 일한다:
161
+ * GraphQL·TypeORM·GC·webpack·HTTP. 그래서 "루프 점유 <1%" 인데 CPU 는 110% 일 수 있고, 실제로 그런
162
+ * 상태를 겪었다. 두 숫자를 나란히 두면 **트윈이 범인인지 아닌지**를 바로 가릴 수 있다.
163
+ *
164
+ * 코어 여러 개를 쓰면 1 을 넘을 수 있다(운영체제의 `%CPU` 와 같은 뜻).
165
+ */
166
+ export type CpuSampler = {
167
+ lastMicros: number;
168
+ lastMs: number;
169
+ };
170
+ /** 첫 표본은 비교 대상이 없다 — `-1` 로 두어 "아직 모른다" 와 0 을 구별한다. */
171
+ export declare function newCpuSampler(): CpuSampler;
172
+ /**
173
+ * 두 표본 사이의 CPU 점유율. 처음 부르면 `null`(0 이 아니다 — 잰 구간이 없다).
174
+ *
175
+ * @param totalMicros `process.cpuUsage()` 의 user + system (마이크로초 누적)
176
+ */
177
+ export declare function sampleCpu(s: CpuSampler, totalMicros: number, nowMs: number): {
178
+ ratio: number;
179
+ spanMs: number;
180
+ } | null;
181
+ export {};
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ /*
3
+ * 부하 계기판 — **어떤 작업이 이벤트 루프를 얼마나 쓰는지** 작업별로 잰다. 순수(import 없음).
4
+ *
5
+ * ── 왜 필요한가 (2026-08-07) ───────────────────────────────────────────────
6
+ * 시뮬 틱은 트윈마다 `setInterval(1000ms)` 로 **메인 이벤트 루프에서 동기로** 수행된다. 부팅 시 16개가
7
+ * 자동 프로비저닝되므로 초당 16번이 같은 루프에서 차례로 실행된다. 한 주기가 예산을 넘기면 타이머가
8
+ * 밀려 쌓이고, 같은 루프에 있는 HTTP·GraphQL·웹소켓이 **함께 멈춘다.**
9
+ *
10
+ * 실제로 그런 상태를 만났다: 포트는 LISTEN 이고 webpack 은 `compiled successfully` 인데 요청이 하나도
11
+ * 처리되지 않고 CPU 만 99% 였다. **터지면 알지만 느려지면 몰랐다** — `tickGuarded` 는 예외만 잡고
12
+ * 시간은 재지 않았고, `TwinMetrics` 에는 라이브 지표(유입·방송·저널)뿐이라 시뮬 틱은 아무도 안 봤다.
13
+ *
14
+ * ── 총량만으로는 부족하다 ─────────────────────────────────────────────────
15
+ * "틱이 800ms 걸렸다" 는 어디를 고칠지 알려 주지 않는다. 한 주기는 여러 작업으로 이뤄진다 —
16
+ * 커널 틱 · 상태 투영(snapshot) · 방송 payload 만들기 · 발행 · 저널 기록 · 유입 흡수. 그래서
17
+ * **작업마다 따로** 잰다. 어느 작업이 총 시간의 몇 %인지 보이면 손댈 곳이 정해진다.
18
+ *
19
+ * 재고 말하는 것이 먼저다. 워커 스레드로 옮기는 것은 그다음이다 — 무엇이 느린지 모른 채 옮기면
20
+ * 느린 채로 옮겨진다.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.WINDOW_MS = exports.WARN_COOLDOWN_MS = exports.TICK_BUDGET_RATIO = exports.LOAD_PHASES = void 0;
24
+ exports.newLoadMeter = newLoadMeter;
25
+ exports.recordPhase = recordPhase;
26
+ exports.recordFork = recordFork;
27
+ exports.rollWindow = rollWindow;
28
+ exports.judgeCycle = judgeCycle;
29
+ exports.loadSummary = loadSummary;
30
+ exports.slowTickMessage = slowTickMessage;
31
+ exports.fleetLoad = fleetLoad;
32
+ exports.newCpuSampler = newCpuSampler;
33
+ exports.sampleCpu = sampleCpu;
34
+ /** 작업 이름 — 새 작업을 재기 시작하면 여기 늘린다(문자열을 자유롭게 쓰지 않는다: 오타가 새 칸이 된다). */
35
+ exports.LOAD_PHASES = ['tick', 'snapshot', 'deltas', 'publish', 'journal', 'ingest', 'forecast', 'forkTick'];
36
+ function newLoadMeter() {
37
+ return { phases: {}, window: { startMs: -1, phases: {} }, recent: null, forksCreated: 0, overBudget: 0, lastWarnMs: -Infinity };
38
+ }
39
+ /**
40
+ * 예산 — 틱 간격의 절반. 넘으면 루프의 절반을 이 트윈 하나가 쓰고 있다는 뜻이다.
41
+ *
42
+ * 절반인 이유: 트윈이 여럿이라 각자 간격을 다 쓰면 이미 포화다. 절반에서 알아야 손 쓸 틈이 있다.
43
+ */
44
+ exports.TICK_BUDGET_RATIO = 0.5;
45
+ /** 경고를 다시 낼 때까지의 최소 간격(ms) — 느린 트윈은 매 틱 느리므로, 그대로 두면 로그가 원인을 덮는다. */
46
+ exports.WARN_COOLDOWN_MS = 30_000;
47
+ /**
48
+ * 한 작업의 소요를 기록한다. **누적만 하고 판단하지 않는다.**
49
+ *
50
+ * 음수·NaN 은 기록하지 않는다(시계가 뒤로 간 경우). 없는 값을 0 으로 세면 평균이 거짓이 된다.
51
+ */
52
+ function recordPhase(meter, phase, tookMs, nowMs = Date.now()) {
53
+ if (!Number.isFinite(tookMs) || tookMs < 0)
54
+ return;
55
+ rollWindow(meter, nowMs);
56
+ for (const bucket of [meter.phases, meter.window.phases]) {
57
+ const s = bucket[phase] ?? (bucket[phase] = { count: 0, lastMs: 0, maxMs: 0, totalMs: 0 });
58
+ s.count += 1;
59
+ s.lastMs = tookMs;
60
+ s.totalMs += tookMs;
61
+ if (tookMs > s.maxMs)
62
+ s.maxMs = tookMs;
63
+ }
64
+ }
65
+ /** fork 하나가 만들어졌다 — 예측 한 번이 수십 개를 만든다. 수를 알아야 부하의 출처를 짚는다. */
66
+ function recordFork(meter, count = 1) {
67
+ meter.forksCreated += count;
68
+ }
69
+ /** 실시간 창의 길이(ms) — 이보다 길게 보려면 누적을 본다. */
70
+ exports.WINDOW_MS = 10_000;
71
+ /** 창이 다 찼으면 닫고 새로 연다. **닫힌 창만 "최근" 으로 읽는다** — 채우는 중인 창은 과소 집계다. */
72
+ function rollWindow(meter, nowMs, windowMs = exports.WINDOW_MS) {
73
+ if (meter.window.startMs < 0) {
74
+ meter.window.startMs = nowMs;
75
+ return;
76
+ }
77
+ if (nowMs - meter.window.startMs < windowMs)
78
+ return;
79
+ meter.recent = { ...meter.window, endMs: nowMs };
80
+ meter.window = { startMs: nowMs, phases: {} };
81
+ }
82
+ /**
83
+ * 이 주기가 예산을 넘었나 — 넘었으면 세고, **말할 때인지**까지 답한다.
84
+ */
85
+ function judgeCycle(meter, tookMs, tickMs, nowMs, budgetRatio = exports.TICK_BUDGET_RATIO, cooldownMs = exports.WARN_COOLDOWN_MS) {
86
+ const budgetMs = tickMs * budgetRatio;
87
+ const over = Number.isFinite(tookMs) && tookMs > budgetMs;
88
+ if (!over)
89
+ return { over: false, warn: false, budgetMs };
90
+ meter.overBudget += 1;
91
+ const warn = nowMs - meter.lastWarnMs >= cooldownMs;
92
+ if (warn)
93
+ meter.lastWarnMs = nowMs;
94
+ return { over: true, warn, budgetMs };
95
+ }
96
+ const round2 = (v) => Math.round(v * 100) / 100;
97
+ /**
98
+ * 비율은 자릿수를 더 준다 — **작은 값이 0 이 되면 "안 쓴다" 로 읽힌다.**
99
+ *
100
+ * 24ms/11초 = 0.0022 인데 소수 둘째 자리로 반올림하면 0 이 되어, 재서 나온 값과 없는 값이 같아진다.
101
+ * 오늘 겪은 착각(0 과 없음을 구별 못 함)과 같은 종류라 여기서 막는다.
102
+ */
103
+ const round4 = (v) => Math.round(v * 10000) / 10000;
104
+ function toRows(phases) {
105
+ const entries = Object.entries(phases).filter(([, s]) => s.count > 0);
106
+ const totalMs = entries.reduce((sum, [, s]) => sum + s.totalMs, 0);
107
+ const rows = entries
108
+ .map(([phase, s]) => ({
109
+ phase,
110
+ count: s.count,
111
+ lastMs: round2(s.lastMs),
112
+ maxMs: round2(s.maxMs),
113
+ avgMs: round2(s.totalMs / s.count),
114
+ totalMs: round2(s.totalMs),
115
+ share: totalMs > 0 ? round4(s.totalMs / totalMs) : 0
116
+ }))
117
+ .sort((a, b) => b.totalMs - a.totalMs);
118
+ return { rows, totalMs };
119
+ }
120
+ function toView(phases, spanMs) {
121
+ const { rows, totalMs } = toRows(phases);
122
+ if (!rows.length)
123
+ return null;
124
+ return {
125
+ phases: rows,
126
+ measuredTotalMs: round2(totalMs),
127
+ spanMs,
128
+ busyRatio: spanMs && spanMs > 0 ? round4(totalMs / spanMs) : null
129
+ };
130
+ }
131
+ /**
132
+ * 읽기용 요약 — **실시간과 누적을 함께** 낸다.
133
+ *
134
+ * · `recent` — 마지막으로 **닫힌** 창. 지금 이 트윈이 루프를 얼마나 쓰고 있나. 채우는 중인 창은
135
+ * 과소 집계라 쓰지 않는다.
136
+ * · `total` — 기동 이후 전부. 총량과 최악값.
137
+ *
138
+ * 잰 적이 없으면 **`null` 이다.** 0 으로 채우면 "0ms 다"(= 아주 빠르다)로 읽혀, 재지 않은 것과 정반대의
139
+ * 뜻이 된다. 몫(`share`)도 잰 작업들 사이의 비율일 뿐 — **재지 않은 작업은 분모에 없다.**
140
+ */
141
+ function loadSummary(meter, tickMs) {
142
+ if (!meter)
143
+ return null;
144
+ const total = toView(meter.phases, null);
145
+ const recent = meter.recent ? toView(meter.recent.phases, meter.recent.endMs - meter.recent.startMs) : null;
146
+ if (!total && !recent && !meter.forksCreated)
147
+ return null;
148
+ const tick = meter.phases.tick;
149
+ return {
150
+ recent,
151
+ total,
152
+ forksCreated: meter.forksCreated,
153
+ overBudget: meter.overBudget,
154
+ budgetMs: tickMs * exports.TICK_BUDGET_RATIO,
155
+ loadRatio: tick && tick.count > 0 ? round4(tick.lastMs / tickMs) : null
156
+ };
157
+ }
158
+ /**
159
+ * 경고 한 줄 — **무엇이 느린지까지 말한다.** "틱이 800ms" 만으로는 어디를 볼지 알 수 없다.
160
+ *
161
+ * 가장 무거운 작업 셋을 몫과 함께 적는다.
162
+ */
163
+ function slowTickMessage(instanceId, tookMs, budgetMs, meter) {
164
+ const { rows } = toRows(meter.recent?.phases ?? meter.phases);
165
+ const top = rows
166
+ .slice(0, 3)
167
+ .map(p => `${p.phase} ${p.avgMs}ms(${Math.round(p.share * 100)}%)`)
168
+ .join(' · ');
169
+ return (`[twin-engine] "${instanceId}" 틱 ${round2(tookMs)}ms — 예산 ${round2(budgetMs)}ms 초과. ` +
170
+ (top ? `무거운 작업: ${top}. ` : '') +
171
+ `시뮬 틱이 메인 이벤트 루프에서 수행됩니다 — 이 상태가 이어지면 HTTP·구독이 함께 느려집니다.`);
172
+ }
173
+ /**
174
+ * 전체 한눈에 — **누가 루프를 점유하고 있나.**
175
+ *
176
+ * 인스턴스 목록을 받아 대수·모드별 수·fork 총수를 세고, **최근 창의 부하가 큰 순서**로 줄을 세운다.
177
+ * 누적이 아니라 최근으로 세우는 이유: 지금 아픈 것을 먼저 봐야 한다(누적 1위는 오래 떠 있던 것일 뿐일
178
+ * 수 있다).
179
+ */
180
+ function fleetLoad(rows, tickMs) {
181
+ const byMode = {};
182
+ const domains = new Map();
183
+ let forksCreated = 0;
184
+ let recentBusyMs = 0;
185
+ let recentSpanMs = null;
186
+ const slowest = rows.map(r => {
187
+ byMode[r.mode] = (byMode[r.mode] ?? 0) + 1;
188
+ forksCreated += r.meter?.forksCreated ?? 0;
189
+ const recent = r.meter?.recent;
190
+ const { rows: phaseRows, totalMs } = toRows(recent?.phases ?? {});
191
+ /*
192
+ * **잎에서 한 번만 반올림한다.** 도메인 합계와 전체 합계는 이 값을 더해서 만든다.
193
+ *
194
+ * 전에는 트윈·도메인·전체를 각자 원값에서 반올림했다. 그래서 화면의 숫자가 서로 안 맞았다:
195
+ * system 도메인 0.08ms 인데 그 도메인의 트윈 둘은 0.08 + 0.01 = 0.09ms (2026-08-07 지적).
196
+ * 보이는 부분들의 합이 보이는 합계와 달라지면 계기판을 믿을 수 없다.
197
+ */
198
+ const busyMs = round2(totalMs);
199
+ if (recent) {
200
+ recentBusyMs += busyMs;
201
+ const span = recent.endMs - recent.startMs;
202
+ /* 창 길이는 트윈마다 조금씩 다를 수 있다 — 가장 긴 것을 쓴다(짧은 것을 쓰면 비율이 부풀려진다). */
203
+ if (span > 0 && (recentSpanMs === null || span > recentSpanMs))
204
+ recentSpanMs = span;
205
+ }
206
+ /* 테넌트별 합산 — 라벨을 모르면 id 를 그대로 쓴다(지어내지 않는다). */
207
+ const key = r.domainId ?? '(unknown)';
208
+ const dom = domains.get(key) ?? { label: r.domainLabel || key, instances: 0, recentBusyMs: 0 };
209
+ dom.instances += 1;
210
+ dom.recentBusyMs += busyMs;
211
+ domains.set(key, dom);
212
+ return {
213
+ instanceId: r.instanceId,
214
+ mode: r.mode,
215
+ domainId: r.domainId,
216
+ domainLabel: r.domainLabel,
217
+ recentBusyMs: busyMs,
218
+ topPhase: phaseRows[0]?.phase ?? null,
219
+ overBudget: r.meter?.overBudget ?? 0
220
+ };
221
+ });
222
+ slowest.sort((a, b) => b.recentBusyMs - a.recentBusyMs);
223
+ const byDomain = [...domains.entries()]
224
+ .map(([domainId, d]) => ({
225
+ domainId,
226
+ label: d.label,
227
+ instances: d.instances,
228
+ recentBusyMs: round2(d.recentBusyMs),
229
+ share: recentBusyMs > 0 ? round4(d.recentBusyMs / recentBusyMs) : 0
230
+ }))
231
+ .sort((a, b) => b.recentBusyMs - a.recentBusyMs);
232
+ return {
233
+ instances: rows.length,
234
+ byMode,
235
+ byDomain,
236
+ forksCreated,
237
+ recentBusyMs: round2(recentBusyMs),
238
+ recentSpanMs,
239
+ fleetBusyRatio: recentSpanMs && recentSpanMs > 0 ? round4(recentBusyMs / recentSpanMs) : null,
240
+ slowest
241
+ };
242
+ }
243
+ /** 첫 표본은 비교 대상이 없다 — `-1` 로 두어 "아직 모른다" 와 0 을 구별한다. */
244
+ function newCpuSampler() {
245
+ return { lastMicros: -1, lastMs: -1 };
246
+ }
247
+ /**
248
+ * 두 표본 사이의 CPU 점유율. 처음 부르면 `null`(0 이 아니다 — 잰 구간이 없다).
249
+ *
250
+ * @param totalMicros `process.cpuUsage()` 의 user + system (마이크로초 누적)
251
+ */
252
+ function sampleCpu(s, totalMicros, nowMs) {
253
+ const prevMicros = s.lastMicros;
254
+ const prevMs = s.lastMs;
255
+ s.lastMicros = totalMicros;
256
+ s.lastMs = nowMs;
257
+ if (prevMicros < 0 || prevMs < 0)
258
+ return null;
259
+ const spanMs = nowMs - prevMs;
260
+ if (spanMs <= 0)
261
+ return null;
262
+ const usedMs = (totalMicros - prevMicros) / 1000;
263
+ if (usedMs < 0)
264
+ return null;
265
+ return { ratio: round4(usedMs / spanMs), spanMs: Math.round(spanMs) };
266
+ }
267
+ //# sourceMappingURL=load-meter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-meter.js","sourceRoot":"","sources":["../../server/engine/load-meter.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 부하 계기판 — **어떤 작업이 이벤트 루프를 얼마나 쓰는지** 작업별로 잰다. 순수(import 없음).\n *\n * ── 왜 필요한가 (2026-08-07) ───────────────────────────────────────────────\n * 시뮬 틱은 트윈마다 `setInterval(1000ms)` 로 **메인 이벤트 루프에서 동기로** 수행된다. 부팅 시 16개가\n * 자동 프로비저닝되므로 초당 16번이 같은 루프에서 차례로 실행된다. 한 주기가 예산을 넘기면 타이머가\n * 밀려 쌓이고, 같은 루프에 있는 HTTP·GraphQL·웹소켓이 **함께 멈춘다.**\n *\n * 실제로 그런 상태를 만났다: 포트는 LISTEN 이고 webpack 은 `compiled successfully` 인데 요청이 하나도\n * 처리되지 않고 CPU 만 99% 였다. **터지면 알지만 느려지면 몰랐다** — `tickGuarded` 는 예외만 잡고\n * 시간은 재지 않았고, `TwinMetrics` 에는 라이브 지표(유입·방송·저널)뿐이라 시뮬 틱은 아무도 안 봤다.\n *\n * ── 총량만으로는 부족하다 ─────────────────────────────────────────────────\n * \"틱이 800ms 걸렸다\" 는 어디를 고칠지 알려 주지 않는다. 한 주기는 여러 작업으로 이뤄진다 —\n * 커널 틱 · 상태 투영(snapshot) · 방송 payload 만들기 · 발행 · 저널 기록 · 유입 흡수. 그래서\n * **작업마다 따로** 잰다. 어느 작업이 총 시간의 몇 %인지 보이면 손댈 곳이 정해진다.\n *\n * 재고 말하는 것이 먼저다. 워커 스레드로 옮기는 것은 그다음이다 — 무엇이 느린지 모른 채 옮기면\n * 느린 채로 옮겨진다.\n */\n\n/** 작업 이름 — 새 작업을 재기 시작하면 여기 늘린다(문자열을 자유롭게 쓰지 않는다: 오타가 새 칸이 된다). */\nexport const LOAD_PHASES = ['tick', 'snapshot', 'deltas', 'publish', 'journal', 'ingest', 'forecast', 'forkTick'] as const\nexport type LoadPhase = (typeof LOAD_PHASES)[number]\n\nexport interface PhaseStats {\n /** 잰 횟수. */\n count: number\n /** 마지막 소요(ms). */\n lastMs: number\n /** 가장 오래 걸린 것(ms) — 평균은 순간 정체를 가린다. */\n maxMs: number\n /** 소요 합(ms). 나눗셈은 읽는 쪽에서 한다. */\n totalMs: number\n}\n\n/** 창(window) 하나의 누적 — \"지금 얼마나 쓰고 있나\" 를 답한다. */\ninterface Window {\n /** 창이 열린 시각. **아직 안 열렸으면 `-1`** — 0 으로 두면 시각 0 과 구별이 안 된다(검사가 잡았다). */\n startMs: number\n phases: Partial<Record<LoadPhase, PhaseStats>>\n}\n\nexport interface LoadMeter {\n /** **누적** — 프로세스가 뜬 뒤 전부. 총량과 최악값을 본다. */\n phases: Partial<Record<LoadPhase, PhaseStats>>\n /** 지금 쌓고 있는 창. */\n window: Window\n /** **마지막으로 닫힌 창** — 이것이 \"실시간\" 이다. 없으면 아직 한 창도 안 지났다. */\n recent: (Window & { endMs: number }) | null\n /** 이 트윈에서 만들어진 fork 수(누적) — 예측 한 번이 수십 개를 만든다. */\n forksCreated: number\n /** 한 주기(틱)가 예산을 넘긴 횟수. */\n overBudget: number\n /**\n * 마지막 경고 시각(ms). **한 번도 안 했으면 `-Infinity`** — 0 으로 두면 \"0 시점에 경고했다\" 가 되어\n * 첫 느린 틱이 조용히 지나간다(검사가 그것을 잡았다).\n */\n lastWarnMs: number\n}\n\nexport function newLoadMeter(): LoadMeter {\n return { phases: {}, window: { startMs: -1, phases: {} }, recent: null, forksCreated: 0, overBudget: 0, lastWarnMs: -Infinity }\n}\n\n/**\n * 예산 — 틱 간격의 절반. 넘으면 루프의 절반을 이 트윈 하나가 쓰고 있다는 뜻이다.\n *\n * 절반인 이유: 트윈이 여럿이라 각자 간격을 다 쓰면 이미 포화다. 절반에서 알아야 손 쓸 틈이 있다.\n */\nexport const TICK_BUDGET_RATIO = 0.5\n\n/** 경고를 다시 낼 때까지의 최소 간격(ms) — 느린 트윈은 매 틱 느리므로, 그대로 두면 로그가 원인을 덮는다. */\nexport const WARN_COOLDOWN_MS = 30_000\n\n/**\n * 한 작업의 소요를 기록한다. **누적만 하고 판단하지 않는다.**\n *\n * 음수·NaN 은 기록하지 않는다(시계가 뒤로 간 경우). 없는 값을 0 으로 세면 평균이 거짓이 된다.\n */\nexport function recordPhase(meter: LoadMeter, phase: LoadPhase, tookMs: number, nowMs = Date.now()): void {\n if (!Number.isFinite(tookMs) || tookMs < 0) return\n\n rollWindow(meter, nowMs)\n for (const bucket of [meter.phases, meter.window.phases]) {\n const s = bucket[phase] ?? (bucket[phase] = { count: 0, lastMs: 0, maxMs: 0, totalMs: 0 })\n s.count += 1\n s.lastMs = tookMs\n s.totalMs += tookMs\n if (tookMs > s.maxMs) s.maxMs = tookMs\n }\n}\n\n/** fork 하나가 만들어졌다 — 예측 한 번이 수십 개를 만든다. 수를 알아야 부하의 출처를 짚는다. */\nexport function recordFork(meter: LoadMeter, count = 1): void {\n meter.forksCreated += count\n}\n\n/** 실시간 창의 길이(ms) — 이보다 길게 보려면 누적을 본다. */\nexport const WINDOW_MS = 10_000\n\n/** 창이 다 찼으면 닫고 새로 연다. **닫힌 창만 \"최근\" 으로 읽는다** — 채우는 중인 창은 과소 집계다. */\nexport function rollWindow(meter: LoadMeter, nowMs: number, windowMs = WINDOW_MS): void {\n if (meter.window.startMs < 0) {\n meter.window.startMs = nowMs\n return\n }\n if (nowMs - meter.window.startMs < windowMs) return\n\n meter.recent = { ...meter.window, endMs: nowMs }\n meter.window = { startMs: nowMs, phases: {} }\n}\n\n/**\n * 이 주기가 예산을 넘었나 — 넘었으면 세고, **말할 때인지**까지 답한다.\n */\nexport function judgeCycle(\n meter: LoadMeter,\n tookMs: number,\n tickMs: number,\n nowMs: number,\n budgetRatio = TICK_BUDGET_RATIO,\n cooldownMs = WARN_COOLDOWN_MS\n): { over: boolean; warn: boolean; budgetMs: number } {\n const budgetMs = tickMs * budgetRatio\n const over = Number.isFinite(tookMs) && tookMs > budgetMs\n if (!over) return { over: false, warn: false, budgetMs }\n\n meter.overBudget += 1\n const warn = nowMs - meter.lastWarnMs >= cooldownMs\n if (warn) meter.lastWarnMs = nowMs\n\n return { over: true, warn, budgetMs }\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100\n/**\n * 비율은 자릿수를 더 준다 — **작은 값이 0 이 되면 \"안 쓴다\" 로 읽힌다.**\n *\n * 24ms/11초 = 0.0022 인데 소수 둘째 자리로 반올림하면 0 이 되어, 재서 나온 값과 없는 값이 같아진다.\n * 오늘 겪은 착각(0 과 없음을 구별 못 함)과 같은 종류라 여기서 막는다.\n */\nconst round4 = (v: number) => Math.round(v * 10000) / 10000\n\nexport interface PhaseRow {\n phase: LoadPhase\n count: number\n lastMs: number\n maxMs: number\n avgMs: number\n totalMs: number\n /** 잰 작업 전체 시간 중 이 작업의 몫(0~1). 어디를 고칠지 이 값이 정한다. */\n share: number\n}\n\nexport interface LoadView {\n phases: PhaseRow[]\n measuredTotalMs: number\n /** 이 구간의 길이(ms). 누적은 프로세스 기동 이후라 알 수 없으므로 `null`. */\n spanMs: number | null\n /** 구간 중 몇 %를 이 트윈이 썼나(0~1+). 구간을 모르면 `null` — **0 이 아니다.** */\n busyRatio: number | null\n}\n\nfunction toRows(phases: Partial<Record<LoadPhase, PhaseStats>>): { rows: PhaseRow[]; totalMs: number } {\n const entries = (Object.entries(phases) as [LoadPhase, PhaseStats][]).filter(([, s]) => s.count > 0)\n const totalMs = entries.reduce((sum, [, s]) => sum + s.totalMs, 0)\n const rows = entries\n .map(([phase, s]) => ({\n phase,\n count: s.count,\n lastMs: round2(s.lastMs),\n maxMs: round2(s.maxMs),\n avgMs: round2(s.totalMs / s.count),\n totalMs: round2(s.totalMs),\n share: totalMs > 0 ? round4(s.totalMs / totalMs) : 0\n }))\n .sort((a, b) => b.totalMs - a.totalMs)\n\n return { rows, totalMs }\n}\n\nfunction toView(phases: Partial<Record<LoadPhase, PhaseStats>>, spanMs: number | null): LoadView | null {\n const { rows, totalMs } = toRows(phases)\n if (!rows.length) return null\n\n return {\n phases: rows,\n measuredTotalMs: round2(totalMs),\n spanMs,\n busyRatio: spanMs && spanMs > 0 ? round4(totalMs / spanMs) : null\n }\n}\n\n/**\n * 읽기용 요약 — **실시간과 누적을 함께** 낸다.\n *\n * · `recent` — 마지막으로 **닫힌** 창. 지금 이 트윈이 루프를 얼마나 쓰고 있나. 채우는 중인 창은\n * 과소 집계라 쓰지 않는다.\n * · `total` — 기동 이후 전부. 총량과 최악값.\n *\n * 잰 적이 없으면 **`null` 이다.** 0 으로 채우면 \"0ms 다\"(= 아주 빠르다)로 읽혀, 재지 않은 것과 정반대의\n * 뜻이 된다. 몫(`share`)도 잰 작업들 사이의 비율일 뿐 — **재지 않은 작업은 분모에 없다.**\n */\nexport function loadSummary(meter: LoadMeter | undefined, tickMs: number): {\n recent: LoadView | null\n total: LoadView | null\n forksCreated: number\n overBudget: number\n budgetMs: number\n /** 마지막 틱이 간격을 얼마나 썼나(0~1+). 1 이면 간격을 통째로 쓴다. */\n loadRatio: number | null\n} | null {\n if (!meter) return null\n\n const total = toView(meter.phases, null)\n const recent = meter.recent ? toView(meter.recent.phases, meter.recent.endMs - meter.recent.startMs) : null\n if (!total && !recent && !meter.forksCreated) return null\n\n const tick = meter.phases.tick\n return {\n recent,\n total,\n forksCreated: meter.forksCreated,\n overBudget: meter.overBudget,\n budgetMs: tickMs * TICK_BUDGET_RATIO,\n loadRatio: tick && tick.count > 0 ? round4(tick.lastMs / tickMs) : null\n }\n}\n\n/**\n * 경고 한 줄 — **무엇이 느린지까지 말한다.** \"틱이 800ms\" 만으로는 어디를 볼지 알 수 없다.\n *\n * 가장 무거운 작업 셋을 몫과 함께 적는다.\n */\nexport function slowTickMessage(instanceId: string, tookMs: number, budgetMs: number, meter: LoadMeter): string {\n const { rows } = toRows(meter.recent?.phases ?? meter.phases)\n const top = rows\n .slice(0, 3)\n .map(p => `${p.phase} ${p.avgMs}ms(${Math.round(p.share * 100)}%)`)\n .join(' · ')\n\n return (\n `[twin-engine] \"${instanceId}\" 틱 ${round2(tookMs)}ms — 예산 ${round2(budgetMs)}ms 초과. ` +\n (top ? `무거운 작업: ${top}. ` : '') +\n `시뮬 틱이 메인 이벤트 루프에서 수행됩니다 — 이 상태가 이어지면 HTTP·구독이 함께 느려집니다.`\n )\n}\n\n/**\n * 전체 한눈에 — **누가 루프를 점유하고 있나.**\n *\n * 인스턴스 목록을 받아 대수·모드별 수·fork 총수를 세고, **최근 창의 부하가 큰 순서**로 줄을 세운다.\n * 누적이 아니라 최근으로 세우는 이유: 지금 아픈 것을 먼저 봐야 한다(누적 1위는 오래 떠 있던 것일 뿐일\n * 수 있다).\n */\nexport function fleetLoad(\n rows: { instanceId: string; mode: string; domainId?: string; domainLabel?: string; meter?: LoadMeter }[],\n tickMs: number\n): {\n instances: number\n byMode: Record<string, number>\n /**\n * **테넌트별 몫** — 운영자가 먼저 묻는 것은 \"어느 테넌트가 루프를 먹나\" 다.\n *\n * 실측으로 필요해졌다: 한 도메인 트윈 22개가 CPU 를 110% 쓰는 동안 다른 도메인 화면은 0 이었다.\n * 무거운 순으로 낸다. 몫은 **잰 트윈들 사이의 비율**이다(안 잰 트윈은 분모에 없다).\n */\n byDomain: { domainId: string; label: string; instances: number; recentBusyMs: number; share: number }[]\n forksCreated: number\n /** 최근 창에서 트윈들이 쓴 시간의 합(ms) — 창 길이를 넘으면 루프가 이미 밀리고 있다. */\n recentBusyMs: number\n /** 그 창의 길이(ms). 창이 하나도 안 닫혔으면 `null`. */\n recentSpanMs: number | null\n /** 전체가 루프를 얼마나 쓰나(0~1+). 1 을 넘으면 **한 틱 안에 다 못 끝낸다.** */\n fleetBusyRatio: number | null\n slowest: { instanceId: string; mode: string; domainId?: string; domainLabel?: string; recentBusyMs: number; topPhase: LoadPhase | null; overBudget: number }[]\n} {\n const byMode: Record<string, number> = {}\n const domains = new Map<string, { label: string; instances: number; recentBusyMs: number }>()\n let forksCreated = 0\n let recentBusyMs = 0\n let recentSpanMs: number | null = null\n\n const slowest = rows.map(r => {\n byMode[r.mode] = (byMode[r.mode] ?? 0) + 1\n forksCreated += r.meter?.forksCreated ?? 0\n\n const recent = r.meter?.recent\n const { rows: phaseRows, totalMs } = toRows(recent?.phases ?? {})\n /*\n * **잎에서 한 번만 반올림한다.** 도메인 합계와 전체 합계는 이 값을 더해서 만든다.\n *\n * 전에는 트윈·도메인·전체를 각자 원값에서 반올림했다. 그래서 화면의 숫자가 서로 안 맞았다:\n * system 도메인 0.08ms 인데 그 도메인의 트윈 둘은 0.08 + 0.01 = 0.09ms (2026-08-07 지적).\n * 보이는 부분들의 합이 보이는 합계와 달라지면 계기판을 믿을 수 없다.\n */\n const busyMs = round2(totalMs)\n if (recent) {\n recentBusyMs += busyMs\n const span = recent.endMs - recent.startMs\n /* 창 길이는 트윈마다 조금씩 다를 수 있다 — 가장 긴 것을 쓴다(짧은 것을 쓰면 비율이 부풀려진다). */\n if (span > 0 && (recentSpanMs === null || span > recentSpanMs)) recentSpanMs = span\n }\n\n /* 테넌트별 합산 — 라벨을 모르면 id 를 그대로 쓴다(지어내지 않는다). */\n const key = r.domainId ?? '(unknown)'\n const dom = domains.get(key) ?? { label: r.domainLabel || key, instances: 0, recentBusyMs: 0 }\n dom.instances += 1\n dom.recentBusyMs += busyMs\n domains.set(key, dom)\n\n return {\n instanceId: r.instanceId,\n mode: r.mode,\n domainId: r.domainId,\n domainLabel: r.domainLabel,\n recentBusyMs: busyMs,\n topPhase: phaseRows[0]?.phase ?? null,\n overBudget: r.meter?.overBudget ?? 0\n }\n })\n\n slowest.sort((a, b) => b.recentBusyMs - a.recentBusyMs)\n\n const byDomain = [...domains.entries()]\n .map(([domainId, d]) => ({\n domainId,\n label: d.label,\n instances: d.instances,\n recentBusyMs: round2(d.recentBusyMs),\n share: recentBusyMs > 0 ? round4(d.recentBusyMs / recentBusyMs) : 0\n }))\n .sort((a, b) => b.recentBusyMs - a.recentBusyMs)\n\n return {\n instances: rows.length,\n byMode,\n byDomain,\n forksCreated,\n recentBusyMs: round2(recentBusyMs),\n recentSpanMs,\n fleetBusyRatio: recentSpanMs && recentSpanMs > 0 ? round4(recentBusyMs / recentSpanMs) : null,\n slowest\n }\n}\n\n/**\n * 프로세스가 실제로 쓴 CPU — **계기판의 다른 숫자들과 뜻이 다르다.**\n *\n * 이 파일의 다른 값들은 트윈의 **계측된 8개 작업**만 잰 것이다. 그런데 프로세스는 그 밖에도 일한다:\n * GraphQL·TypeORM·GC·webpack·HTTP. 그래서 \"루프 점유 <1%\" 인데 CPU 는 110% 일 수 있고, 실제로 그런\n * 상태를 겪었다. 두 숫자를 나란히 두면 **트윈이 범인인지 아닌지**를 바로 가릴 수 있다.\n *\n * 코어 여러 개를 쓰면 1 을 넘을 수 있다(운영체제의 `%CPU` 와 같은 뜻).\n */\nexport type CpuSampler = { lastMicros: number; lastMs: number }\n\n/** 첫 표본은 비교 대상이 없다 — `-1` 로 두어 \"아직 모른다\" 와 0 을 구별한다. */\nexport function newCpuSampler(): CpuSampler {\n return { lastMicros: -1, lastMs: -1 }\n}\n\n/**\n * 두 표본 사이의 CPU 점유율. 처음 부르면 `null`(0 이 아니다 — 잰 구간이 없다).\n *\n * @param totalMicros `process.cpuUsage()` 의 user + system (마이크로초 누적)\n */\nexport function sampleCpu(s: CpuSampler, totalMicros: number, nowMs: number): { ratio: number; spanMs: number } | null {\n const prevMicros = s.lastMicros\n const prevMs = s.lastMs\n s.lastMicros = totalMicros\n s.lastMs = nowMs\n\n if (prevMicros < 0 || prevMs < 0) return null\n\n const spanMs = nowMs - prevMs\n if (spanMs <= 0) return null\n\n const usedMs = (totalMicros - prevMicros) / 1000\n if (usedMs < 0) return null\n\n return { ratio: round4(usedMs / spanMs), spanMs: Math.round(spanMs) }\n}\n"]}