@things-factory/headless-twin 10.0.12 → 10.0.14

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 (315) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +57 -2
  2. package/dist-server/engine/canonical-ingest.js +43 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/index.d.ts +3 -0
  7. package/dist-server/engine/index.js +4 -0
  8. package/dist-server/engine/index.js.map +1 -1
  9. package/dist-server/engine/ingest-health.d.ts +376 -0
  10. package/dist-server/engine/ingest-health.js +482 -0
  11. package/dist-server/engine/ingest-health.js.map +1 -0
  12. package/dist-server/engine/integration-coverage.d.ts +76 -0
  13. package/dist-server/engine/integration-coverage.js +73 -0
  14. package/dist-server/engine/integration-coverage.js.map +1 -0
  15. package/dist-server/engine/integration-probes.d.ts +65 -0
  16. package/dist-server/engine/integration-probes.js +100 -0
  17. package/dist-server/engine/integration-probes.js.map +1 -0
  18. package/dist-server/engine/integration-runner.d.ts +45 -0
  19. package/dist-server/engine/integration-runner.js +59 -0
  20. package/dist-server/engine/integration-runner.js.map +1 -0
  21. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  22. package/dist-server/engine/integration-target-profile.js +79 -0
  23. package/dist-server/engine/integration-target-profile.js.map +1 -0
  24. package/dist-server/engine/kpi-fold.d.ts +27 -0
  25. package/dist-server/engine/kpi-fold.js +47 -4
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.js +128 -14
  28. package/dist-server/engine/kpi-query.js.map +1 -1
  29. package/dist-server/engine/live-feed-registry.js +2 -16
  30. package/dist-server/engine/live-feed-registry.js.map +1 -1
  31. package/dist-server/engine/load-meter.d.ts +1 -1
  32. package/dist-server/engine/load-meter.js +12 -3
  33. package/dist-server/engine/load-meter.js.map +1 -1
  34. package/dist-server/engine/log.d.ts +18 -0
  35. package/dist-server/engine/log.js +80 -0
  36. package/dist-server/engine/log.js.map +1 -0
  37. package/dist-server/engine/loop-lag.d.ts +54 -0
  38. package/dist-server/engine/loop-lag.js +87 -0
  39. package/dist-server/engine/loop-lag.js.map +1 -0
  40. package/dist-server/engine/model-gap.d.ts +108 -0
  41. package/dist-server/engine/model-gap.js +95 -0
  42. package/dist-server/engine/model-gap.js.map +1 -0
  43. package/dist-server/engine/restart-policy.d.ts +3 -3
  44. package/dist-server/engine/restart-policy.js +4 -4
  45. package/dist-server/engine/restart-policy.js.map +1 -1
  46. package/dist-server/engine/runtime-key.js +1 -1
  47. package/dist-server/engine/runtime-key.js.map +1 -1
  48. package/dist-server/engine/stage-path.d.ts +83 -0
  49. package/dist-server/engine/stage-path.js +118 -0
  50. package/dist-server/engine/stage-path.js.map +1 -0
  51. package/dist-server/engine/twin-engine.d.ts +386 -16
  52. package/dist-server/engine/twin-engine.js +1217 -172
  53. package/dist-server/engine/twin-engine.js.map +1 -1
  54. package/dist-server/index.js +34 -2
  55. package/dist-server/index.js.map +1 -1
  56. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  57. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  58. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  59. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  60. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  61. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  62. package/dist-server/migrations/index.js +7 -1
  63. package/dist-server/migrations/index.js.map +1 -1
  64. package/dist-server/service/index.d.ts +5 -2
  65. package/dist-server/service/index.js +18 -7
  66. package/dist-server/service/index.js.map +1 -1
  67. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  68. package/dist-server/service/reference/discovery-result.js +1 -1
  69. package/dist-server/service/reference/discovery-result.js.map +1 -1
  70. package/dist-server/service/reference/index.d.ts +4 -1
  71. package/dist-server/service/reference/index.js +6 -1
  72. package/dist-server/service/reference/index.js.map +1 -1
  73. package/dist-server/service/reference/reference-adapter.d.ts +210 -3
  74. package/dist-server/service/reference/reference-adapter.js +28 -4
  75. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  76. package/dist-server/service/reference/reference-assessment.d.ts +78 -0
  77. package/dist-server/service/reference/reference-assessment.js +156 -0
  78. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  79. package/dist-server/service/reference/reference-live.d.ts +12 -1
  80. package/dist-server/service/reference/reference-live.js +134 -11
  81. package/dist-server/service/reference/reference-live.js.map +1 -1
  82. package/dist-server/service/reference/reference-master.d.ts +59 -3
  83. package/dist-server/service/reference/reference-master.js +34 -4
  84. package/dist-server/service/reference/reference-master.js.map +1 -1
  85. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  86. package/dist-server/service/reference/reference-probe.js +186 -0
  87. package/dist-server/service/reference/reference-probe.js.map +1 -0
  88. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  89. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  90. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  91. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  92. package/dist-server/service/reference/reference-progress.js +71 -0
  93. package/dist-server/service/reference/reference-progress.js.map +1 -0
  94. package/dist-server/service/reference/reference-resolver.js +210 -43
  95. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  96. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  97. package/dist-server/service/reference/twin-reference.js +10 -0
  98. package/dist-server/service/reference/twin-reference.js.map +1 -1
  99. package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
  100. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  101. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  102. package/dist-server/service/twin-event/journal-count.js +26 -0
  103. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  104. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  105. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  106. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  107. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  108. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  109. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  110. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  111. package/dist-server/service/twin-event/twin-event.js +9 -1
  112. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  113. package/dist-server/service/twin-forecast/twin-forecast-query.js +26 -2
  114. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  115. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  116. package/dist-server/service/twin-ingest-window/index.js +10 -0
  117. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  118. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  119. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  120. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  121. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  122. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  123. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  124. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  125. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  126. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  127. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  128. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  129. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  130. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  131. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  133. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  134. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  135. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  136. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  137. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  138. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  139. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  140. package/dist-server/service/twin-model/epcis-coverage.js +46 -7
  141. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  142. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  143. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  144. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  145. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  146. package/dist-server/service/twin-model/isa95-coverage.js +108 -14
  147. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  148. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  149. package/dist-server/service/twin-model/item-ref.js +88 -0
  150. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  151. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  152. package/dist-server/service/twin-model/name-index.js +116 -0
  153. package/dist-server/service/twin-model/name-index.js.map +1 -0
  154. package/dist-server/service/twin-model/project-structure.js +1 -1
  155. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  156. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  157. package/dist-server/service/twin-model/status-tally.js +37 -0
  158. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  160. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  161. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  162. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  163. package/dist-server/service/twin-model/twin-model-query.js +156 -4
  164. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  165. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  166. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  167. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  168. package/dist-server/service/twin-readiness/index.js +6 -0
  169. package/dist-server/service/twin-readiness/index.js.map +1 -0
  170. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  171. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  172. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  173. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  174. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  175. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  176. package/dist-shared/entity-delta.d.ts +23 -3
  177. package/dist-shared/entity-delta.js +12 -8
  178. package/dist-shared/entity-delta.js.map +1 -1
  179. package/dist-shared/kpi-broadcast.js +1 -1
  180. package/dist-shared/kpi-broadcast.js.map +1 -1
  181. package/dist-shared/touched-items.d.ts +7 -0
  182. package/dist-shared/touched-items.js +80 -0
  183. package/dist-shared/touched-items.js.map +1 -0
  184. package/package.json +7 -7
  185. package/server/engine/canonical-ingest.ts +92 -7
  186. package/server/engine/command-routing.ts +1 -1
  187. package/server/engine/index.ts +4 -0
  188. package/server/engine/ingest-health.ts +782 -0
  189. package/server/engine/integration-coverage.ts +147 -0
  190. package/server/engine/integration-probes.ts +144 -0
  191. package/server/engine/integration-runner.ts +95 -0
  192. package/server/engine/integration-target-profile.ts +103 -0
  193. package/server/engine/kpi-fold.ts +72 -4
  194. package/server/engine/kpi-query.ts +129 -15
  195. package/server/engine/live-feed-registry.ts +2 -1
  196. package/server/engine/load-meter.ts +12 -3
  197. package/server/engine/log.ts +72 -0
  198. package/server/engine/loop-lag.ts +120 -0
  199. package/server/engine/model-gap.ts +168 -0
  200. package/server/engine/restart-policy.ts +4 -4
  201. package/server/engine/runtime-key.ts +1 -1
  202. package/server/engine/stage-path.ts +172 -0
  203. package/server/engine/twin-engine.ts +1334 -168
  204. package/server/index.ts +35 -2
  205. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  206. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  207. package/server/migrations/index.ts +7 -1
  208. package/server/service/index.ts +11 -0
  209. package/server/service/reference/discovery-result.ts +1 -1
  210. package/server/service/reference/index.ts +6 -1
  211. package/server/service/reference/reference-adapter.ts +220 -5
  212. package/server/service/reference/reference-assessment.ts +250 -0
  213. package/server/service/reference/reference-live.ts +143 -11
  214. package/server/service/reference/reference-master.ts +76 -6
  215. package/server/service/reference/reference-probe.ts +264 -0
  216. package/server/service/reference/reference-progress-subscription.ts +73 -0
  217. package/server/service/reference/reference-progress.ts +95 -0
  218. package/server/service/reference/reference-resolver.ts +200 -12
  219. package/server/service/reference/twin-reference.ts +39 -1
  220. package/server/service/twin-control/twin-control-mutation.ts +4 -3
  221. package/server/service/twin-event/journal-count.ts +53 -0
  222. package/server/service/twin-event/twin-event-keys.ts +16 -1
  223. package/server/service/twin-event/twin-event-type.ts +26 -2
  224. package/server/service/twin-event/twin-event.ts +7 -0
  225. package/server/service/twin-forecast/twin-forecast-query.ts +29 -2
  226. package/server/service/twin-ingest-window/index.ts +7 -0
  227. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  228. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  229. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  230. package/server/service/twin-instance/twin-instance.ts +1 -1
  231. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  232. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
  233. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  234. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  235. package/server/service/twin-model/epcis-coverage.ts +29 -8
  236. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  237. package/server/service/twin-model/isa95-coverage.ts +107 -14
  238. package/server/service/twin-model/item-ref.ts +80 -0
  239. package/server/service/twin-model/name-index.ts +94 -0
  240. package/server/service/twin-model/project-structure.ts +1 -1
  241. package/server/service/twin-model/status-tally.ts +37 -0
  242. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  243. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  244. package/server/service/twin-model/twin-model-query.ts +155 -4
  245. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  246. package/server/service/twin-readiness/index.ts +3 -0
  247. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  248. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  249. package/shared/entity-delta.ts +31 -7
  250. package/shared/kpi-broadcast.ts +1 -1
  251. package/shared/touched-items.ts +73 -0
  252. package/test/axis-journal-evidence.test.ts +71 -0
  253. package/test/axis-read.test.ts +101 -1
  254. package/test/boot-resume.test.ts +92 -26
  255. package/test/broadcast-cost-baseline.test.ts +200 -0
  256. package/test/broadcast-period.test.ts +58 -0
  257. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  258. package/test/canonical-quantity-door.test.ts +61 -0
  259. package/test/checkpoint-refuses-empty.test.ts +103 -0
  260. package/test/command-routing.test.ts +1 -1
  261. package/test/cursor-stall-not-read-failure.test.ts +126 -0
  262. package/test/declared-location-types.test.ts +89 -0
  263. package/test/discovery-result.test.ts +1 -1
  264. package/test/duration-estimators.test.ts +1 -1
  265. package/test/entity-delta.test.ts +2 -2
  266. package/test/ingest-bench.test.ts +3 -3
  267. package/test/ingest-health-engine.test.ts +248 -0
  268. package/test/ingest-health-wiring.test.ts +119 -0
  269. package/test/ingest-health.test.ts +306 -0
  270. package/test/ingest-history.test.ts +247 -0
  271. package/test/integration-probes.test.ts +103 -0
  272. package/test/integration-runner.test.ts +95 -0
  273. package/test/item-ref.test.ts +78 -0
  274. package/test/journal-read-discipline.test.ts +177 -0
  275. package/test/journal-retention.test.ts +133 -0
  276. package/test/journal-sort-axis.test.ts +142 -0
  277. package/test/journal-write-door.test.ts +110 -0
  278. package/test/journal-write-trend.test.ts +115 -0
  279. package/test/kernel-kind-guard.test.ts +4 -4
  280. package/test/kpi-fold.test.ts +90 -3
  281. package/test/kpi-query-bench.test.ts +2 -2
  282. package/test/lineage-survives-restart.test.ts +17 -1
  283. package/test/live-cursor-wiring.test.ts +87 -0
  284. package/test/live-feed-registry.test.ts +9 -1
  285. package/test/live-kernel-facts.test.ts +7 -7
  286. package/test/live-mirror-parity.test.ts +6 -0
  287. package/test/load-meter.test.ts +29 -15
  288. package/test/local-declarations.test.ts +1 -1
  289. package/test/log-stamp.test.ts +59 -0
  290. package/test/loop-lag.test.ts +82 -0
  291. package/test/mirror-resumes-from-checkpoint.test.ts +192 -0
  292. package/test/model-gap.test.ts +153 -0
  293. package/test/oee-accumulator.test.ts +4 -0
  294. package/test/operational-vocabulary.test.ts +4 -4
  295. package/test/read-failure-visible.test.ts +145 -0
  296. package/test/reference-grounding.test.ts +70 -0
  297. package/test/resolve-ts-siblings.mjs +52 -0
  298. package/test/restart-policy.test.ts +7 -7
  299. package/test/revision-axis.test.ts +104 -0
  300. package/test/runtime-key.test.ts +2 -2
  301. package/test/scale-twin-bench.test.ts +2 -2
  302. package/test/spec-coverage.test.ts +1 -1
  303. package/test/stage-path.test.ts +95 -0
  304. package/test/standard-coverage.test.ts +15 -3
  305. package/test/status-tally.test.ts +55 -0
  306. package/test/structure-revision-db.test.ts +4 -0
  307. package/test/tenant-registry-db.test.ts +1 -1
  308. package/test/time-range.test.ts +152 -0
  309. package/test/touched-items.test.ts +61 -0
  310. package/test/twin-event-keys.test.ts +10 -2
  311. package/test/twin-model-item-db.test.ts +2 -0
  312. package/test/warm-start-seam.test.ts +4 -0
  313. package/test/yield-loop.test.ts +57 -2
  314. package/tsconfig.shared.tsbuildinfo +1 -1
  315. package/tsconfig.tsbuildinfo +1 -1
@@ -9,11 +9,11 @@
9
9
  *
10
10
  * 실제로 그런 상태를 만났다: 포트는 LISTEN 이고 webpack 은 `compiled successfully` 인데 요청이 하나도
11
11
  * 처리되지 않고 CPU 만 99% 였다. **터지면 알지만 느려지면 몰랐다** — `tickGuarded` 는 예외만 잡고
12
- * 시간은 재지 않았고, `TwinMetrics` 에는 라이브 지표(유입·방송·저널)뿐이라 시뮬 틱은 아무도 안 봤다.
12
+ * 시간은 재지 않았고, `TwinMetrics` 에는 라이브 지표(유입·브로드캐스팅·저널)뿐이라 시뮬 틱은 아무도 안 봤다.
13
13
  *
14
14
  * ── 총량만으로는 부족하다 ─────────────────────────────────────────────────
15
15
  * "틱이 800ms 걸렸다" 는 어디를 고칠지 알려 주지 않는다. 한 주기는 여러 작업으로 이뤄진다 —
16
- * 커널 틱 · 상태 투영(snapshot) · 방송 payload 만들기 · 발행 · 저널 기록 · 유입 흡수. 그래서
16
+ * 커널 틱 · 상태 투영(snapshot) · 브로드캐스팅 payload 만들기 · 발행 · 저널 기록 · 유입 흡수. 그래서
17
17
  * **작업마다 따로** 잰다. 어느 작업이 총 시간의 몇 %인지 보이면 손댈 곳이 정해진다.
18
18
  *
19
19
  * 재고 말하는 것이 먼저다. 워커 스레드로 옮기는 것은 그다음이다 — 무엇이 느린지 모른 채 옮기면
@@ -32,7 +32,16 @@ exports.fleetLoad = fleetLoad;
32
32
  exports.newCpuSampler = newCpuSampler;
33
33
  exports.sampleCpu = sampleCpu;
34
34
  /** 작업 이름 — 새 작업을 재기 시작하면 여기 늘린다(문자열을 자유롭게 쓰지 않는다: 오타가 새 칸이 된다). */
35
- exports.LOAD_PHASES = ['tick', 'snapshot', 'deltas', 'publish', 'journal', 'ingest', 'forecast', 'forkTick'];
35
+ /*
36
+ * ── 부팅 구간을 재는 두 이름 (2026-08-22) ───────────────────────────────────
37
+ * 실측으로 부팅이 620~717초였고 트윈 하나를 세우는 데 15~93초였다. 그런데 **어느 작업이 그 시간을
38
+ * 쓰는지 아무 계기도 답하지 못했다** — 재는 이름이 도는 중의 작업(틱·브로드캐스팅·저널)뿐이었다.
39
+ * 짐작으로 원인을 적으면 그것이 사실로 읽히므로, 그 자리에 이름을 둔다.
40
+ *
41
+ * · `warmStart` — 저장된 상태를 커널에 심는다(물품·오더·작업·주목 시각).
42
+ * · `stimulus` — 선언된 자극을 싣고 시작한다(실측: 큰 트윈에서 이 뒤가 29~72초였다).
43
+ */
44
+ exports.LOAD_PHASES = ['tick', 'snapshot', 'deltas', 'publish', 'journal', 'ingest', 'forecast', 'forkTick', 'warmStart', 'stimulus'];
36
45
  function newLoadMeter() {
37
46
  return { phases: {}, window: { startMs: -1, phases: {} }, recent: null, forksCreated: 0, overBudget: 0, lastWarnMs: -Infinity };
38
47
  }
@@ -1 +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"]}
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/** 작업 이름 — 새 작업을 재기 시작하면 여기 늘린다(문자열을 자유롭게 쓰지 않는다: 오타가 새 칸이 된다). */\n/*\n * ── 부팅 구간을 재는 두 이름 (2026-08-22) ───────────────────────────────────\n * 실측으로 부팅이 620~717초였고 트윈 하나를 세우는 데 15~93초였다. 그런데 **어느 작업이 그 시간을\n * 쓰는지 아무 계기도 답하지 못했다** — 재는 이름이 도는 중의 작업(틱·브로드캐스팅·저널)뿐이었다.\n * 짐작으로 원인을 적으면 그것이 사실로 읽히므로, 그 자리에 이름을 둔다.\n *\n * · `warmStart` — 저장된 상태를 커널에 심는다(물품·오더·작업·주목 시각).\n * · `stimulus` — 선언된 자극을 싣고 시작한다(실측: 큰 트윈에서 이 뒤가 29~72초였다).\n */\nexport const LOAD_PHASES = ['tick', 'snapshot', 'deltas', 'publish', 'journal', 'ingest', 'forecast', 'forkTick', 'warmStart', 'stimulus'] 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"]}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * 지역 시각 + 오프셋 — `2026-08-22T05:54:12.345+09:00`.
3
+ *
4
+ * UTC 로 찍지 않는다: 이 로그를 읽는 사람은 현장의 시각으로 이야기하고, 프레임워크의 다른 줄도
5
+ * 지역 시각이다. 두 줄이 다른 기준이면 나란히 놓고 읽을 수 없다.
6
+ */
7
+ export declare function stamp(at?: Date): string;
8
+ /**
9
+ * 인자 여럿을 한 줄로 — **어떤 인자도 버리지 않는다.**
10
+ *
11
+ * `console.error(msg, err)` 처럼 쓰던 자리가 많다. 프레임워크 로거에 그대로 넘기면 두 번째 인자는
12
+ * meta 로 들어가 우리 형식(`message` 만 찍는다)에서 **조용히 사라진다.** 그래서 여기서 문자열로 편다.
13
+ * 오류는 스택까지 — 스택이 없으면 「어디서 났나」를 잃는다.
14
+ */
15
+ export declare function line(args: unknown[]): string;
16
+ export declare const twinLog: (...args: unknown[]) => void;
17
+ export declare const twinWarn: (...args: unknown[]) => void;
18
+ export declare const twinError: (...args: unknown[]) => void;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /*
3
+ * 트윈 로그의 문 — **모든 줄에 시각을 찍는다.** 순수(프레임워크 의존 없음).
4
+ *
5
+ * ── 왜 필요한가 (2026-08-22) ────────────────────────────────────────────────
6
+ * 「서버가 왜 느린가」를 조사하면서 루프가 55초 막힌 것을 계기로 알아냈는데(`loop-lag.ts`),
7
+ * **그 시각에 무슨 일이 있었는지 로그에서 찾을 수 없었다.** 트윈의 로그가 `console.log` 로 나가
8
+ * 시각이 없었기 때문이다. 프레임워크 로그(`2026-08-22T05:54:12+09:00 info: …`)와 섞여 있어서, 어느
9
+ * 줄이 언제 찍혔는지 앞뒤 프레임워크 줄로 짐작해야 했다.
10
+ *
11
+ * ── 왜 프레임워크 로거를 그대로 쓰지 않나 ──────────────────────────────────
12
+ * 재발명을 피하려고 `@things-factory/env` 의 `logger` 를 쓰려 했는데, 실측으로 막혔다: 앱 **밖**에서는
13
+ * 그 로거에 전송지가 하나도 없다(설정이 앱에서 주입된다). 그래서 시험·도구에서 부르면
14
+ *
15
+ * [winston] Attempt to write logs with no transports … {"message":"…","level":"info"}
16
+ *
17
+ * 가 나오고 **메시지 자체가 사라진다.** 트윈 엔진은 시험과 CLI 도구에서도 불려 다니므로, 그 조건에서
18
+ * 로그가 없어지는 것은 바꿔선 안 되는 거동이다(로그가 사라지는 것이 지금 문제의 원인이었다).
19
+ *
20
+ * 그래서 **싱크는 그대로 두고**(console) 빠진 사실 하나만 채운다 — 시각. 형식은 프레임워크와 같은
21
+ * 모양으로 맞춰 한 파일에서 섞여 읽히게 하고, **밀리초까지** 남긴다(계기의 값이 epoch ms 라
22
+ * 초 단위로는 같은 초 안의 순서를 가릴 수 없다).
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.twinError = exports.twinWarn = exports.twinLog = void 0;
26
+ exports.stamp = stamp;
27
+ exports.line = line;
28
+ /** 두 자리·세 자리 0 채우기 — 형식이 흔들리면 정렬해 읽을 수 없다. */
29
+ const p2 = (n) => String(n).padStart(2, '0');
30
+ const p3 = (n) => String(n).padStart(3, '0');
31
+ /**
32
+ * 지역 시각 + 오프셋 — `2026-08-22T05:54:12.345+09:00`.
33
+ *
34
+ * UTC 로 찍지 않는다: 이 로그를 읽는 사람은 현장의 시각으로 이야기하고, 프레임워크의 다른 줄도
35
+ * 지역 시각이다. 두 줄이 다른 기준이면 나란히 놓고 읽을 수 없다.
36
+ */
37
+ function stamp(at = new Date()) {
38
+ const off = -at.getTimezoneOffset();
39
+ const sign = off >= 0 ? '+' : '-';
40
+ const abs = Math.abs(off);
41
+ return (`${at.getFullYear()}-${p2(at.getMonth() + 1)}-${p2(at.getDate())}` +
42
+ `T${p2(at.getHours())}:${p2(at.getMinutes())}:${p2(at.getSeconds())}.${p3(at.getMilliseconds())}` +
43
+ `${sign}${p2(Math.floor(abs / 60))}:${p2(abs % 60)}`);
44
+ }
45
+ /**
46
+ * 인자 여럿을 한 줄로 — **어떤 인자도 버리지 않는다.**
47
+ *
48
+ * `console.error(msg, err)` 처럼 쓰던 자리가 많다. 프레임워크 로거에 그대로 넘기면 두 번째 인자는
49
+ * meta 로 들어가 우리 형식(`message` 만 찍는다)에서 **조용히 사라진다.** 그래서 여기서 문자열로 편다.
50
+ * 오류는 스택까지 — 스택이 없으면 「어디서 났나」를 잃는다.
51
+ */
52
+ function line(args) {
53
+ return args
54
+ .map(a => {
55
+ if (typeof a === 'string')
56
+ return a;
57
+ if (a instanceof Error)
58
+ return a.stack ?? a.message;
59
+ if (a === undefined)
60
+ return 'undefined';
61
+ if (a === null)
62
+ return 'null';
63
+ try {
64
+ return typeof a === 'object' ? JSON.stringify(a) : String(a);
65
+ }
66
+ catch {
67
+ /* 순환 참조 등 — 값을 못 적어도 **무엇이 있었다는 사실**은 남긴다. */
68
+ return String(a);
69
+ }
70
+ })
71
+ .join(' ');
72
+ }
73
+ /* 수준 표기는 프레임워크와 같은 낱말을 쓴다(`info`·`warn`·`error`) — 같은 파일에서 걸러 읽는다. */
74
+ const twinLog = (...args) => console.log(`${stamp()} info: ${line(args)}`);
75
+ exports.twinLog = twinLog;
76
+ const twinWarn = (...args) => console.warn(`${stamp()} warn: ${line(args)}`);
77
+ exports.twinWarn = twinWarn;
78
+ const twinError = (...args) => console.error(`${stamp()} error: ${line(args)}`);
79
+ exports.twinError = twinError;
80
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../server/engine/log.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;;;AAYH,sBASC;AASD,oBAeC;AA3CD,8CAA8C;AAC9C,MAAM,EAAE,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AACpD,MAAM,EAAE,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AAEpD;;;;;GAKG;AACH,SAAgB,KAAK,CAAC,KAAW,IAAI,IAAI,EAAE;IACzC,MAAM,GAAG,GAAG,CAAC,EAAE,CAAC,iBAAiB,EAAE,CAAA;IACnC,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;IACjC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACzB,OAAO,CACL,GAAG,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE;QAClE,IAAI,EAAE,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,EAAE;QACjG,GAAG,IAAI,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,EAAE,CACrD,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,IAAI,CAAC,IAAe;IAClC,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,CAAC,EAAE;QACP,IAAI,OAAO,CAAC,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAA;QACnC,IAAI,CAAC,YAAY,KAAK;YAAE,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,CAAA;QACnD,IAAI,CAAC,KAAK,SAAS;YAAE,OAAO,WAAW,CAAA;QACvC,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,MAAM,CAAA;QAC7B,IAAI,CAAC;YACH,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,8CAA8C;YAC9C,OAAO,MAAM,CAAC,CAAC,CAAC,CAAA;QAClB,CAAC;IACH,CAAC,CAAC;SACD,IAAI,CAAC,GAAG,CAAC,CAAA;AACd,CAAC;AAED,sEAAsE;AAC/D,MAAM,OAAO,GAAG,CAAC,GAAG,IAAe,EAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,EAAE,UAAU,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAArF,QAAA,OAAO,WAA8E;AAC3F,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAe,EAAQ,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,EAAE,UAAU,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAAvF,QAAA,QAAQ,YAA+E;AAC7F,MAAM,SAAS,GAAG,CAAC,GAAG,IAAe,EAAQ,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,EAAE,WAAW,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAA1F,QAAA,SAAS,aAAiF","sourcesContent":["/*\n * 트윈 로그의 문 — **모든 줄에 시각을 찍는다.** 순수(프레임워크 의존 없음).\n *\n * ── 왜 필요한가 (2026-08-22) ────────────────────────────────────────────────\n * 「서버가 왜 느린가」를 조사하면서 루프가 55초 막힌 것을 계기로 알아냈는데(`loop-lag.ts`),\n * **그 시각에 무슨 일이 있었는지 로그에서 찾을 수 없었다.** 트윈의 로그가 `console.log` 로 나가\n * 시각이 없었기 때문이다. 프레임워크 로그(`2026-08-22T05:54:12+09:00 info: …`)와 섞여 있어서, 어느\n * 줄이 언제 찍혔는지 앞뒤 프레임워크 줄로 짐작해야 했다.\n *\n * ── 왜 프레임워크 로거를 그대로 쓰지 않나 ──────────────────────────────────\n * 재발명을 피하려고 `@things-factory/env` 의 `logger` 를 쓰려 했는데, 실측으로 막혔다: 앱 **밖**에서는\n * 그 로거에 전송지가 하나도 없다(설정이 앱에서 주입된다). 그래서 시험·도구에서 부르면\n *\n * [winston] Attempt to write logs with no transports … {\"message\":\"…\",\"level\":\"info\"}\n *\n * 가 나오고 **메시지 자체가 사라진다.** 트윈 엔진은 시험과 CLI 도구에서도 불려 다니므로, 그 조건에서\n * 로그가 없어지는 것은 바꿔선 안 되는 거동이다(로그가 사라지는 것이 지금 문제의 원인이었다).\n *\n * 그래서 **싱크는 그대로 두고**(console) 빠진 사실 하나만 채운다 — 시각. 형식은 프레임워크와 같은\n * 모양으로 맞춰 한 파일에서 섞여 읽히게 하고, **밀리초까지** 남긴다(계기의 값이 epoch ms 라\n * 초 단위로는 같은 초 안의 순서를 가릴 수 없다).\n */\n\n/** 두 자리·세 자리 0 채우기 — 형식이 흔들리면 정렬해 읽을 수 없다. */\nconst p2 = (n: number) => String(n).padStart(2, '0')\nconst p3 = (n: number) => String(n).padStart(3, '0')\n\n/**\n * 지역 시각 + 오프셋 — `2026-08-22T05:54:12.345+09:00`.\n *\n * UTC 로 찍지 않는다: 이 로그를 읽는 사람은 현장의 시각으로 이야기하고, 프레임워크의 다른 줄도\n * 지역 시각이다. 두 줄이 다른 기준이면 나란히 놓고 읽을 수 없다.\n */\nexport function stamp(at: Date = new Date()): string {\n const off = -at.getTimezoneOffset()\n const sign = off >= 0 ? '+' : '-'\n const abs = Math.abs(off)\n return (\n `${at.getFullYear()}-${p2(at.getMonth() + 1)}-${p2(at.getDate())}` +\n `T${p2(at.getHours())}:${p2(at.getMinutes())}:${p2(at.getSeconds())}.${p3(at.getMilliseconds())}` +\n `${sign}${p2(Math.floor(abs / 60))}:${p2(abs % 60)}`\n )\n}\n\n/**\n * 인자 여럿을 한 줄로 — **어떤 인자도 버리지 않는다.**\n *\n * `console.error(msg, err)` 처럼 쓰던 자리가 많다. 프레임워크 로거에 그대로 넘기면 두 번째 인자는\n * meta 로 들어가 우리 형식(`message` 만 찍는다)에서 **조용히 사라진다.** 그래서 여기서 문자열로 편다.\n * 오류는 스택까지 — 스택이 없으면 「어디서 났나」를 잃는다.\n */\nexport function line(args: unknown[]): string {\n return args\n .map(a => {\n if (typeof a === 'string') return a\n if (a instanceof Error) return a.stack ?? a.message\n if (a === undefined) return 'undefined'\n if (a === null) return 'null'\n try {\n return typeof a === 'object' ? JSON.stringify(a) : String(a)\n } catch {\n /* 순환 참조 등 — 값을 못 적어도 **무엇이 있었다는 사실**은 남긴다. */\n return String(a)\n }\n })\n .join(' ')\n}\n\n/* 수준 표기는 프레임워크와 같은 낱말을 쓴다(`info`·`warn`·`error`) — 같은 파일에서 걸러 읽는다. */\nexport const twinLog = (...args: unknown[]): void => console.log(`${stamp()} info: ${line(args)}`)\nexport const twinWarn = (...args: unknown[]): void => console.warn(`${stamp()} warn: ${line(args)}`)\nexport const twinError = (...args: unknown[]): void => console.error(`${stamp()} error: ${line(args)}`)\n"]}
@@ -0,0 +1,54 @@
1
+ /** 표본 간격(ms) — 촘촘할 필요가 없다. 긴 정체를 놓치지 않는 정도면 된다. */
2
+ export declare const LAG_SAMPLE_MS = 500;
3
+ /** 이 값을 넘는 지체만 「정체」로 센다(ms). 사람이 화면에서 느끼기 시작하는 크기. */
4
+ export declare const LAG_STALL_MS = 200;
5
+ /** 창 길이(ms) — `twinLoad` 의 창과 같은 크기로 두어 나란히 읽는다. */
6
+ export declare const LAG_WINDOW_MS = 10000;
7
+ export interface LoopLagWindow {
8
+ /** 이 창에서 가장 길게 막힌 시간(ms). */
9
+ maxMs: number;
10
+ /** 정체(문턱 초과) 횟수. */
11
+ stalls: number;
12
+ /** 표본 수 — 분모다. */
13
+ samples: number;
14
+ /** 지체 합(ms) — 평균은 읽는 쪽에서 낸다. */
15
+ totalMs: number;
16
+ /** 창이 열린 시각. */
17
+ startMs: number;
18
+ }
19
+ export interface LoopLagView {
20
+ /** 마지막으로 닫힌 창 — 「지금 어떤가」. 아직 한 창도 안 지났으면 null. */
21
+ recent: (LoopLagWindow & {
22
+ endMs: number;
23
+ }) | null;
24
+ /** 기동 이후 누적. */
25
+ total: LoopLagWindow;
26
+ /** 기동 이후 가장 길게 막힌 시각(ms, epoch) — 언제였는지가 원인을 가리는 데 쓰인다. */
27
+ worstAtMs: number | null;
28
+ /** 표본 간격 — 읽는 쪽이 이 값보다 짧은 정체는 못 본다는 것을 알아야 한다. */
29
+ sampleMs: number;
30
+ /** 정체로 세는 문턱. */
31
+ stallMs: number;
32
+ }
33
+ export declare class LoopLagSampler {
34
+ private window;
35
+ private closed;
36
+ private total;
37
+ private worstAtMs;
38
+ private timer;
39
+ private expectedMs;
40
+ private sampleMs;
41
+ private stallMs;
42
+ private windowMs;
43
+ private now;
44
+ constructor(sampleMs?: number, stallMs?: number, windowMs?: number, now?: () => number);
45
+ /** 표본을 하나 넣는다 — 타이머가 부르고, 시험은 직접 부른다(시계를 밀어 넣어 잰다). */
46
+ sample(atMs?: number): number;
47
+ /** 계기판이 읽는 값. **한 창도 닫히지 않았으면 `recent` 는 null** — 채우는 중인 창은 과소 집계다. */
48
+ view(): LoopLagView;
49
+ /** 표본 타이머 기동(1회) — 프로세스 종료를 막지 않는다. */
50
+ start(): void;
51
+ stop(): void;
52
+ }
53
+ /** 프로세스 하나의 루프를 재는 것이므로 표본기도 하나다. */
54
+ export declare const loopLag: LoopLagSampler;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /*
3
+ * 이벤트 루프 지체 — **화면이 느린 이유를 지어내지 않기 위해.** 순수(import 없음).
4
+ *
5
+ * ── 왜 필요한가 (2026-08-21) ────────────────────────────────────────────────
6
+ * 「UI 반응성이 느리다」를 진단하려 했는데, 있는 계기판으로는 답이 나오지 않았다. `twinLoad` 는 **트윈의
7
+ * 계측된 작업**만 재고, `processCpuRatio` 는 프로세스 전체를 재지만 **언제 얼마나 멈췄나**를 말하지
8
+ * 못한다. 밖에서 HTTP 지연을 재 보니 같은 요청이 44ms 와 5.8초로 갈라졌는데, 그 5.8초가 트윈 때문인지
9
+ * webpack 재빌드 때문인지 남의 재기동 때문인지 **가릴 근거가 없었다.**
10
+ *
11
+ * 근거 없이 원인을 고르면 엉뚱한 곳을 고친다. 그래서 루프가 실제로 얼마나 막혔는지를 잰다.
12
+ *
13
+ * ── 어떻게 재나 ─────────────────────────────────────────────────────────────
14
+ * 일정 간격으로 깨어나는 타이머를 두고, **예정 시각과 실제 시각의 차이**를 본다. 루프가 막혀 있었으면
15
+ * 그만큼 늦게 깨어난다. 재는 비용은 그 차이 계산 하나뿐이다(창마다 값 다섯).
16
+ *
17
+ * 이 값은 **원인을 말하지 않는다** — 「200ms 이상 막힌 일이 이 창에서 세 번, 가장 긴 것은 4.6초」까지가
18
+ * 사실이다. 원인은 그 시각의 트윈 부하(`twinLoad`)와 프로세스 CPU 와 나란히 놓고 사람이 가린다.
19
+ * 재지 않은 것을 0 으로 적지 않는다 — 한 번도 깨어나지 않았으면 `null` 이다.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.loopLag = exports.LoopLagSampler = exports.LAG_WINDOW_MS = exports.LAG_STALL_MS = exports.LAG_SAMPLE_MS = void 0;
23
+ /** 표본 간격(ms) — 촘촘할 필요가 없다. 긴 정체를 놓치지 않는 정도면 된다. */
24
+ exports.LAG_SAMPLE_MS = 500;
25
+ /** 이 값을 넘는 지체만 「정체」로 센다(ms). 사람이 화면에서 느끼기 시작하는 크기. */
26
+ exports.LAG_STALL_MS = 200;
27
+ /** 창 길이(ms) — `twinLoad` 의 창과 같은 크기로 두어 나란히 읽는다. */
28
+ exports.LAG_WINDOW_MS = 10_000;
29
+ const emptyWindow = (startMs) => ({ maxMs: 0, stalls: 0, samples: 0, totalMs: 0, startMs });
30
+ class LoopLagSampler {
31
+ /* 생성자 파라미터 프로퍼티(`private x = …`)는 쓰지 않는다 — 시험이 소스를 그대로 실행하는데
32
+ (타입 제거 실행 방식) 그 문법은 지원되지 않아 파일 전체가 열리지 않는다. */
33
+ constructor(sampleMs = exports.LAG_SAMPLE_MS, stallMs = exports.LAG_STALL_MS, windowMs = exports.LAG_WINDOW_MS, now = Date.now) {
34
+ this.closed = null;
35
+ this.worstAtMs = null;
36
+ this.sampleMs = sampleMs;
37
+ this.stallMs = stallMs;
38
+ this.windowMs = windowMs;
39
+ this.now = now;
40
+ const t = this.now();
41
+ this.window = emptyWindow(t);
42
+ this.total = emptyWindow(t);
43
+ this.expectedMs = t + sampleMs;
44
+ }
45
+ /** 표본을 하나 넣는다 — 타이머가 부르고, 시험은 직접 부른다(시계를 밀어 넣어 잰다). */
46
+ sample(atMs = this.now()) {
47
+ const lag = Math.max(0, atMs - this.expectedMs);
48
+ this.expectedMs = atMs + this.sampleMs;
49
+ for (const w of [this.window, this.total]) {
50
+ w.samples += 1;
51
+ w.totalMs += lag;
52
+ if (lag > this.stallMs)
53
+ w.stalls += 1;
54
+ if (lag > w.maxMs)
55
+ w.maxMs = lag;
56
+ }
57
+ if (lag >= this.total.maxMs && lag > this.stallMs)
58
+ this.worstAtMs = atMs;
59
+ if (atMs - this.window.startMs >= this.windowMs) {
60
+ this.closed = { ...this.window, endMs: atMs };
61
+ this.window = emptyWindow(atMs);
62
+ }
63
+ return lag;
64
+ }
65
+ /** 계기판이 읽는 값. **한 창도 닫히지 않았으면 `recent` 는 null** — 채우는 중인 창은 과소 집계다. */
66
+ view() {
67
+ return { recent: this.closed, total: this.total, worstAtMs: this.worstAtMs, sampleMs: this.sampleMs, stallMs: this.stallMs };
68
+ }
69
+ /** 표본 타이머 기동(1회) — 프로세스 종료를 막지 않는다. */
70
+ start() {
71
+ if (this.timer)
72
+ return;
73
+ this.timer = setInterval(() => this.sample(), this.sampleMs);
74
+ if (typeof this.timer?.unref === 'function')
75
+ this.timer.unref();
76
+ }
77
+ stop() {
78
+ if (!this.timer)
79
+ return;
80
+ clearInterval(this.timer);
81
+ this.timer = undefined;
82
+ }
83
+ }
84
+ exports.LoopLagSampler = LoopLagSampler;
85
+ /** 프로세스 하나의 루프를 재는 것이므로 표본기도 하나다. */
86
+ exports.loopLag = new LoopLagSampler();
87
+ //# sourceMappingURL=loop-lag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop-lag.js","sourceRoot":"","sources":["../../server/engine/loop-lag.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 이벤트 루프 지체 — **화면이 느린 이유를 지어내지 않기 위해.** 순수(import 없음).\n *\n * ── 왜 필요한가 (2026-08-21) ────────────────────────────────────────────────\n * 「UI 반응성이 느리다」를 진단하려 했는데, 있는 계기판으로는 답이 나오지 않았다. `twinLoad` 는 **트윈의\n * 계측된 작업**만 재고, `processCpuRatio` 는 프로세스 전체를 재지만 **언제 얼마나 멈췄나**를 말하지\n * 못한다. 밖에서 HTTP 지연을 재 보니 같은 요청이 44ms 와 5.8초로 갈라졌는데, 그 5.8초가 트윈 때문인지\n * webpack 재빌드 때문인지 남의 재기동 때문인지 **가릴 근거가 없었다.**\n *\n * 근거 없이 원인을 고르면 엉뚱한 곳을 고친다. 그래서 루프가 실제로 얼마나 막혔는지를 잰다.\n *\n * ── 어떻게 재나 ─────────────────────────────────────────────────────────────\n * 일정 간격으로 깨어나는 타이머를 두고, **예정 시각과 실제 시각의 차이**를 본다. 루프가 막혀 있었으면\n * 그만큼 늦게 깨어난다. 재는 비용은 그 차이 계산 하나뿐이다(창마다 값 다섯).\n *\n * 이 값은 **원인을 말하지 않는다** — 「200ms 이상 막힌 일이 이 창에서 세 번, 가장 긴 것은 4.6초」까지가\n * 사실이다. 원인은 그 시각의 트윈 부하(`twinLoad`)와 프로세스 CPU 와 나란히 놓고 사람이 가린다.\n * 재지 않은 것을 0 으로 적지 않는다 — 한 번도 깨어나지 않았으면 `null` 이다.\n */\n\n/** 표본 간격(ms) — 촘촘할 필요가 없다. 긴 정체를 놓치지 않는 정도면 된다. */\nexport const LAG_SAMPLE_MS = 500\n/** 이 값을 넘는 지체만 「정체」로 센다(ms). 사람이 화면에서 느끼기 시작하는 크기. */\nexport const LAG_STALL_MS = 200\n/** 창 길이(ms) — `twinLoad` 의 창과 같은 크기로 두어 나란히 읽는다. */\nexport const LAG_WINDOW_MS = 10_000\n\nexport interface LoopLagWindow {\n /** 이 창에서 가장 길게 막힌 시간(ms). */\n maxMs: number\n /** 정체(문턱 초과) 횟수. */\n stalls: number\n /** 표본 수 — 분모다. */\n samples: number\n /** 지체 합(ms) — 평균은 읽는 쪽에서 낸다. */\n totalMs: number\n /** 창이 열린 시각. */\n startMs: number\n}\n\nexport interface LoopLagView {\n /** 마지막으로 닫힌 창 — 「지금 어떤가」. 아직 한 창도 안 지났으면 null. */\n recent: (LoopLagWindow & { endMs: number }) | null\n /** 기동 이후 누적. */\n total: LoopLagWindow\n /** 기동 이후 가장 길게 막힌 시각(ms, epoch) — 언제였는지가 원인을 가리는 데 쓰인다. */\n worstAtMs: number | null\n /** 표본 간격 — 읽는 쪽이 이 값보다 짧은 정체는 못 본다는 것을 알아야 한다. */\n sampleMs: number\n /** 정체로 세는 문턱. */\n stallMs: number\n}\n\nconst emptyWindow = (startMs: number): LoopLagWindow => ({ maxMs: 0, stalls: 0, samples: 0, totalMs: 0, startMs })\n\nexport class LoopLagSampler {\n private window: LoopLagWindow\n private closed: (LoopLagWindow & { endMs: number }) | null = null\n private total: LoopLagWindow\n private worstAtMs: number | null = null\n private timer: any\n private expectedMs: number\n\n private sampleMs: number\n private stallMs: number\n private windowMs: number\n private now: () => number\n\n /* 생성자 파라미터 프로퍼티(`private x = …`)는 쓰지 않는다 — 시험이 소스를 그대로 실행하는데\n (타입 제거 실행 방식) 그 문법은 지원되지 않아 파일 전체가 열리지 않는다. */\n constructor(sampleMs = LAG_SAMPLE_MS, stallMs = LAG_STALL_MS, windowMs = LAG_WINDOW_MS, now: () => number = Date.now) {\n this.sampleMs = sampleMs\n this.stallMs = stallMs\n this.windowMs = windowMs\n this.now = now\n const t = this.now()\n this.window = emptyWindow(t)\n this.total = emptyWindow(t)\n this.expectedMs = t + sampleMs\n }\n\n /** 표본을 하나 넣는다 — 타이머가 부르고, 시험은 직접 부른다(시계를 밀어 넣어 잰다). */\n sample(atMs = this.now()): number {\n const lag = Math.max(0, atMs - this.expectedMs)\n this.expectedMs = atMs + this.sampleMs\n for (const w of [this.window, this.total]) {\n w.samples += 1\n w.totalMs += lag\n if (lag > this.stallMs) w.stalls += 1\n if (lag > w.maxMs) w.maxMs = lag\n }\n if (lag >= this.total.maxMs && lag > this.stallMs) this.worstAtMs = atMs\n if (atMs - this.window.startMs >= this.windowMs) {\n this.closed = { ...this.window, endMs: atMs }\n this.window = emptyWindow(atMs)\n }\n return lag\n }\n\n /** 계기판이 읽는 값. **한 창도 닫히지 않았으면 `recent` 는 null** — 채우는 중인 창은 과소 집계다. */\n view(): LoopLagView {\n return { recent: this.closed, total: this.total, worstAtMs: this.worstAtMs, sampleMs: this.sampleMs, stallMs: this.stallMs }\n }\n\n /** 표본 타이머 기동(1회) — 프로세스 종료를 막지 않는다. */\n start(): void {\n if (this.timer) return\n this.timer = setInterval(() => this.sample(), this.sampleMs)\n if (typeof this.timer?.unref === 'function') this.timer.unref()\n }\n\n stop(): void {\n if (!this.timer) return\n clearInterval(this.timer)\n this.timer = undefined\n }\n}\n\n/** 프로세스 하나의 루프를 재는 것이므로 표본기도 하나다. */\nexport const loopLag = new LoopLagSampler()\n"]}
@@ -0,0 +1,108 @@
1
+ /** 커널 스냅샷에서 우리가 읽는 것만. 나머지 필드는 알 필요가 없다. */
2
+ export interface ModelGapSource {
3
+ locations?: {
4
+ id?: string;
5
+ origin?: string;
6
+ }[];
7
+ equipment?: {
8
+ id?: string;
9
+ origin?: string;
10
+ }[];
11
+ persons?: {
12
+ id?: string;
13
+ origin?: string;
14
+ }[];
15
+ /** 커널이 담을 줄 몰라 세어 둔 사건 — 종류별 개수와 처음·마지막 시각. */
16
+ unhandled?: {
17
+ eventType: string;
18
+ count: number;
19
+ firstAtMs?: number;
20
+ lastAtMs?: number;
21
+ }[];
22
+ /**
23
+ * EMS 상태 — **`energy` 안에 있다.**
24
+ *
25
+ * `unknownEquipment` 를 루트에서 읽고 있었다(2026-08-20에 잡음). 커널은 그것을 `EnergyState` 안에
26
+ * 담으므로, 루트에서 읽으면 **영원히 `null`** 이다 — 오류도 없이 「에너지 트윈이 아니다」로 읽힌다.
27
+ * 나만 읽는 필드를 잘못된 경로에서 읽으면 이렇게 조용히 사라진다.
28
+ *
29
+ * 계량 지점의 `origin` 은 **없을 수 있다**(옛 스냅샷에서 이어받은 지점). 그때는 「선언되었다」가
30
+ * 아니라 **모른다**다.
31
+ */
32
+ energy?: {
33
+ points?: {
34
+ id?: string;
35
+ origin?: string;
36
+ }[];
37
+ /** 모델이 모르는 설비가 에너지 상태를 보내 온 횟수. 0 이면 커널이 이 칸을 만들지 않는다. */
38
+ unknownEquipment?: number;
39
+ };
40
+ }
41
+ /**
42
+ * 화면에 내는 id 수 상한.
43
+ *
44
+ * 오타 하나를 찾는 데 스무 개면 충분하고, 원천이 수천 개를 보내는 경우(모델을 아예 안 맞춘 경우)에
45
+ * 목록으로 화면을 채울 이유가 없다. **넘친 수는 함께 낸다** — 조용히 자르지 않는다.
46
+ */
47
+ export declare const GAP_ID_LIMIT = 20;
48
+ export interface ModelGapAxis {
49
+ /** 관측으로만 알게 된 id 수. */
50
+ count: number;
51
+ /** 그중 화면에 싣는 것(최대 `GAP_ID_LIMIT`). */
52
+ ids: string[];
53
+ /** 상한에서 밀려난 수. 0 이면 전부 실었다. */
54
+ more: number;
55
+ /**
56
+ * 출처를 **모르는** 수 — 표시가 없는 항목.
57
+ *
58
+ * 「선언되었다」로 세지 않는다. 계량기는 이 표시가 늦게 생겼으므로 옛 스냅샷에서 이어받은 지점에는
59
+ * 칸이 없다(`undefined`). 그것을 마스터로 세면 오타를 놓치고, 관측으로 세면 정상을 문제로 만든다.
60
+ * 둘 다 틀리므로 **모른다고 센다.**
61
+ */
62
+ unknown: number;
63
+ }
64
+ export interface ModelGapView {
65
+ locations: ModelGapAxis;
66
+ equipment: ModelGapAxis;
67
+ persons: ModelGapAxis;
68
+ /**
69
+ * EMS 계량 지점.
70
+ *
71
+ * ── 이 축이 가장 무겁다 ────────────────────────────────────────────────────
72
+ * 모르는 `meterId` 는 조용히 새 계량기가 되고 그 kW 가 그 현장의 **수요 구간·피크·요금 판정**에
73
+ * 더해진다. 오타 하나면 진짜 계량기는 멈추고 유령이 누적된다 — 숫자는 그럴듯하고 틀린다. 자리 오타는
74
+ * 스키매틱이 비는 것으로 드러나지만, 이건 그럴듯한 숫자로 나온다.
75
+ *
76
+ * 커널이 `origin` 을 **표본마다 다시 맞춘다** — 선언의 정본은 모델이고 모델은 채택으로 늘어나므로,
77
+ * 한 번 찍고 두면 나중에 선언된 계량기가 영원히 `observed` 로 남는다. 그래서 이 값은 「지금 모델에
78
+ * 있는가」의 답이고, 「한 번 오타였다」의 답이 아니다.
79
+ */
80
+ meters: ModelGapAxis;
81
+ /** 세 축의 합 — 화면이 「있나 없나」를 한 번에 묻기 위한 값. */
82
+ total: number;
83
+ /** 커널이 담을 줄 모른 사건들(종류별). 참조 문제와 다른 사실이므로 따로 낸다. */
84
+ unhandled: {
85
+ eventType: string;
86
+ count: number;
87
+ lastAt: string | null;
88
+ }[];
89
+ /** EMS: 모델이 모르는 설비의 에너지 보고 횟수. 없으면 `null`(0 과 구별한다). */
90
+ unknownEquipmentReports: number | null;
91
+ /**
92
+ * 아직 덮지 못하는 축 — **모르는 것을 모른다고 말한다.**
93
+ *
94
+ * 계량기는 커널 0.7.39 부터 덮인다(그 전에는 `origin` 표시가 없어 오타를 찾아 줄 수 없었다).
95
+ * 비어 있는 것이 정상이고, 새 축이 표시 없이 들어오면 여기에 이름을 넣는다 — 화면이 「다 찾아
96
+ * 준다」고 말하지 않게.
97
+ */
98
+ notCovered: string[];
99
+ }
100
+ /** 계량기 축이 아직 덮이지 않는 이유 — 화면이 그대로 말할 수 있게 이름을 여기 둔다. */
101
+ export declare const NOT_COVERED_METERS = "meters";
102
+ /**
103
+ * 스냅샷 → 모델 갭.
104
+ *
105
+ * 스냅샷이 없으면 `null` 이다 — **0 이 아니다.** 트윈이 돌지 않으면 비교할 상대가 없고, 「없다」와
106
+ * 「세어 보지 못했다」는 다른 사실이다(모델 인스펙션이 같은 규율을 쓴다).
107
+ */
108
+ export declare function modelGapOf(snapshot: ModelGapSource | null | undefined): ModelGapView | null;