@things-factory/headless-twin 10.0.11 → 10.0.13

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 (369) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +81 -3
  2. package/dist-server/engine/canonical-ingest.js +89 -10
  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/declared-stimulus.d.ts +43 -0
  7. package/dist-server/engine/declared-stimulus.js +57 -0
  8. package/dist-server/engine/declared-stimulus.js.map +1 -0
  9. package/dist-server/engine/index.d.ts +5 -0
  10. package/dist-server/engine/index.js +6 -0
  11. package/dist-server/engine/index.js.map +1 -1
  12. package/dist-server/engine/ingest-health.d.ts +335 -0
  13. package/dist-server/engine/ingest-health.js +434 -0
  14. package/dist-server/engine/ingest-health.js.map +1 -0
  15. package/dist-server/engine/integration-coverage.d.ts +76 -0
  16. package/dist-server/engine/integration-coverage.js +73 -0
  17. package/dist-server/engine/integration-coverage.js.map +1 -0
  18. package/dist-server/engine/integration-probes.d.ts +65 -0
  19. package/dist-server/engine/integration-probes.js +100 -0
  20. package/dist-server/engine/integration-probes.js.map +1 -0
  21. package/dist-server/engine/integration-runner.d.ts +45 -0
  22. package/dist-server/engine/integration-runner.js +59 -0
  23. package/dist-server/engine/integration-runner.js.map +1 -0
  24. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  25. package/dist-server/engine/integration-target-profile.js +79 -0
  26. package/dist-server/engine/integration-target-profile.js.map +1 -0
  27. package/dist-server/engine/kpi-fold.d.ts +39 -0
  28. package/dist-server/engine/kpi-fold.js +68 -5
  29. package/dist-server/engine/kpi-fold.js.map +1 -1
  30. package/dist-server/engine/kpi-query.d.ts +3 -3
  31. package/dist-server/engine/kpi-query.js +132 -18
  32. package/dist-server/engine/kpi-query.js.map +1 -1
  33. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  34. package/dist-server/engine/live-feed-registry.js +3 -17
  35. package/dist-server/engine/live-feed-registry.js.map +1 -1
  36. package/dist-server/engine/load-meter.d.ts +1 -1
  37. package/dist-server/engine/load-meter.js +12 -3
  38. package/dist-server/engine/load-meter.js.map +1 -1
  39. package/dist-server/engine/local-declarations.d.ts +3 -6
  40. package/dist-server/engine/local-declarations.js +97 -16
  41. package/dist-server/engine/local-declarations.js.map +1 -1
  42. package/dist-server/engine/log.d.ts +18 -0
  43. package/dist-server/engine/log.js +80 -0
  44. package/dist-server/engine/log.js.map +1 -0
  45. package/dist-server/engine/loop-lag.d.ts +54 -0
  46. package/dist-server/engine/loop-lag.js +87 -0
  47. package/dist-server/engine/loop-lag.js.map +1 -0
  48. package/dist-server/engine/measured-yield.d.ts +42 -0
  49. package/dist-server/engine/measured-yield.js +75 -0
  50. package/dist-server/engine/measured-yield.js.map +1 -0
  51. package/dist-server/engine/model-gap.d.ts +108 -0
  52. package/dist-server/engine/model-gap.js +95 -0
  53. package/dist-server/engine/model-gap.js.map +1 -0
  54. package/dist-server/engine/operation-basis.d.ts +16 -0
  55. package/dist-server/engine/operation-basis.js +20 -2
  56. package/dist-server/engine/operation-basis.js.map +1 -1
  57. package/dist-server/engine/property-effects.js +17 -0
  58. package/dist-server/engine/property-effects.js.map +1 -1
  59. package/dist-server/engine/restart-policy.d.ts +13 -0
  60. package/dist-server/engine/restart-policy.js +52 -0
  61. package/dist-server/engine/restart-policy.js.map +1 -0
  62. package/dist-server/engine/runtime-key.js +1 -1
  63. package/dist-server/engine/runtime-key.js.map +1 -1
  64. package/dist-server/engine/spec-coverage.d.ts +8 -0
  65. package/dist-server/engine/spec-coverage.js +3 -1
  66. package/dist-server/engine/spec-coverage.js.map +1 -1
  67. package/dist-server/engine/stage-path.d.ts +83 -0
  68. package/dist-server/engine/stage-path.js +118 -0
  69. package/dist-server/engine/stage-path.js.map +1 -0
  70. package/dist-server/engine/twin-engine.d.ts +400 -31
  71. package/dist-server/engine/twin-engine.js +1198 -187
  72. package/dist-server/engine/twin-engine.js.map +1 -1
  73. package/dist-server/index.js +35 -3
  74. package/dist-server/index.js.map +1 -1
  75. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  76. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  77. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  78. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  79. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  80. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  81. package/dist-server/migrations/index.js +7 -1
  82. package/dist-server/migrations/index.js.map +1 -1
  83. package/dist-server/service/index.d.ts +5 -2
  84. package/dist-server/service/index.js +18 -7
  85. package/dist-server/service/index.js.map +1 -1
  86. package/dist-server/service/reference/control-routing.d.ts +14 -0
  87. package/dist-server/service/reference/control-routing.js +64 -0
  88. package/dist-server/service/reference/control-routing.js.map +1 -0
  89. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  90. package/dist-server/service/reference/discovery-result.js +1 -1
  91. package/dist-server/service/reference/discovery-result.js.map +1 -1
  92. package/dist-server/service/reference/index.d.ts +5 -1
  93. package/dist-server/service/reference/index.js +8 -1
  94. package/dist-server/service/reference/index.js.map +1 -1
  95. package/dist-server/service/reference/reference-adapter.d.ts +251 -3
  96. package/dist-server/service/reference/reference-adapter.js +43 -1
  97. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  98. package/dist-server/service/reference/reference-assessment.d.ts +68 -0
  99. package/dist-server/service/reference/reference-assessment.js +136 -0
  100. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  101. package/dist-server/service/reference/reference-live.d.ts +12 -1
  102. package/dist-server/service/reference/reference-live.js +119 -14
  103. package/dist-server/service/reference/reference-live.js.map +1 -1
  104. package/dist-server/service/reference/reference-master.d.ts +47 -1
  105. package/dist-server/service/reference/reference-master.js +34 -4
  106. package/dist-server/service/reference/reference-master.js.map +1 -1
  107. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  108. package/dist-server/service/reference/reference-probe.js +186 -0
  109. package/dist-server/service/reference/reference-probe.js.map +1 -0
  110. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  111. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  112. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  113. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  114. package/dist-server/service/reference/reference-progress.js +71 -0
  115. package/dist-server/service/reference/reference-progress.js.map +1 -0
  116. package/dist-server/service/reference/reference-resolver.d.ts +3 -3
  117. package/dist-server/service/reference/reference-resolver.js +242 -58
  118. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  119. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  120. package/dist-server/service/reference/twin-reference.js +10 -0
  121. package/dist-server/service/reference/twin-reference.js.map +1 -1
  122. package/dist-server/service/twin-audit/command-audit.d.ts +34 -0
  123. package/dist-server/service/twin-audit/command-audit.js +15 -1
  124. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  125. package/dist-server/service/twin-audit/twin-audit-event.d.ts +3 -0
  126. package/dist-server/service/twin-audit/twin-audit-event.js +10 -0
  127. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  128. package/dist-server/service/twin-control/twin-control-mutation.d.ts +15 -2
  129. package/dist-server/service/twin-control/twin-control-mutation.js +88 -36
  130. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  131. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  132. package/dist-server/service/twin-event/journal-count.js +26 -0
  133. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  134. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  135. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  136. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  137. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  138. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  139. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  140. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  141. package/dist-server/service/twin-event/twin-event.js +9 -1
  142. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  143. package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
  144. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  145. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  146. package/dist-server/service/twin-ingest-window/index.js +10 -0
  147. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  148. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  149. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  150. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  151. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  152. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  153. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  154. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  155. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  156. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  157. package/dist-server/service/twin-instance/twin-instance.d.ts +1 -1
  158. package/dist-server/service/twin-instance/twin-instance.js +2 -2
  159. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  160. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  161. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  162. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  163. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +74 -3
  164. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  165. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  166. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  167. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  168. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  169. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  170. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  171. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  172. package/dist-server/service/twin-model/epcis-coverage.js +19 -7
  173. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  174. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  175. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  176. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  177. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  178. package/dist-server/service/twin-model/isa95-coverage.js +23 -9
  179. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  180. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  181. package/dist-server/service/twin-model/item-ref.js +88 -0
  182. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  183. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  184. package/dist-server/service/twin-model/name-index.js +116 -0
  185. package/dist-server/service/twin-model/name-index.js.map +1 -0
  186. package/dist-server/service/twin-model/project-structure.js +1 -1
  187. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  188. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  189. package/dist-server/service/twin-model/status-tally.js +37 -0
  190. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  191. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  192. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  193. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  194. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  195. package/dist-server/service/twin-model/twin-model-query.js +157 -5
  196. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  197. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  198. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  199. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  200. package/dist-server/service/twin-readiness/index.js +6 -0
  201. package/dist-server/service/twin-readiness/index.js.map +1 -0
  202. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  203. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  204. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  205. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  206. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  207. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  208. package/dist-shared/entity-delta.d.ts +23 -3
  209. package/dist-shared/entity-delta.js +12 -8
  210. package/dist-shared/entity-delta.js.map +1 -1
  211. package/dist-shared/kpi-broadcast.js +1 -1
  212. package/dist-shared/kpi-broadcast.js.map +1 -1
  213. package/dist-shared/touched-items.d.ts +7 -0
  214. package/dist-shared/touched-items.js +80 -0
  215. package/dist-shared/touched-items.js.map +1 -0
  216. package/package.json +7 -7
  217. package/server/engine/canonical-ingest.ts +164 -15
  218. package/server/engine/command-routing.ts +1 -1
  219. package/server/engine/declared-stimulus.ts +66 -0
  220. package/server/engine/index.ts +6 -0
  221. package/server/engine/ingest-health.ts +704 -0
  222. package/server/engine/integration-coverage.ts +147 -0
  223. package/server/engine/integration-probes.ts +144 -0
  224. package/server/engine/integration-runner.ts +95 -0
  225. package/server/engine/integration-target-profile.ts +103 -0
  226. package/server/engine/kpi-fold.ts +101 -5
  227. package/server/engine/kpi-query.ts +137 -23
  228. package/server/engine/live-feed-registry.ts +4 -3
  229. package/server/engine/load-meter.ts +12 -3
  230. package/server/engine/local-declarations.ts +97 -19
  231. package/server/engine/log.ts +72 -0
  232. package/server/engine/loop-lag.ts +120 -0
  233. package/server/engine/measured-yield.ts +89 -0
  234. package/server/engine/model-gap.ts +168 -0
  235. package/server/engine/operation-basis.ts +33 -2
  236. package/server/engine/property-effects.ts +17 -0
  237. package/server/engine/restart-policy.ts +55 -0
  238. package/server/engine/runtime-key.ts +1 -1
  239. package/server/engine/spec-coverage.ts +23 -3
  240. package/server/engine/stage-path.ts +172 -0
  241. package/server/engine/twin-engine.ts +1318 -186
  242. package/server/index.ts +36 -3
  243. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  244. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  245. package/server/migrations/index.ts +7 -1
  246. package/server/service/index.ts +11 -0
  247. package/server/service/reference/control-routing.ts +62 -0
  248. package/server/service/reference/discovery-result.ts +1 -1
  249. package/server/service/reference/index.ts +7 -1
  250. package/server/service/reference/reference-adapter.ts +275 -5
  251. package/server/service/reference/reference-assessment.ts +215 -0
  252. package/server/service/reference/reference-live.ts +126 -14
  253. package/server/service/reference/reference-master.ts +64 -4
  254. package/server/service/reference/reference-probe.ts +264 -0
  255. package/server/service/reference/reference-progress-subscription.ts +73 -0
  256. package/server/service/reference/reference-progress.ts +95 -0
  257. package/server/service/reference/reference-resolver.ts +246 -19
  258. package/server/service/reference/twin-reference.ts +39 -1
  259. package/server/service/twin-audit/command-audit.ts +34 -1
  260. package/server/service/twin-audit/twin-audit-event.ts +20 -0
  261. package/server/service/twin-control/twin-control-mutation.ts +78 -30
  262. package/server/service/twin-event/journal-count.ts +53 -0
  263. package/server/service/twin-event/twin-event-keys.ts +16 -1
  264. package/server/service/twin-event/twin-event-type.ts +26 -2
  265. package/server/service/twin-event/twin-event.ts +7 -0
  266. package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
  267. package/server/service/twin-ingest-window/index.ts +7 -0
  268. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  269. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  270. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  271. package/server/service/twin-instance/twin-instance.ts +16 -10
  272. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  273. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +79 -4
  274. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  275. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  276. package/server/service/twin-model/epcis-coverage.ts +2 -8
  277. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  278. package/server/service/twin-model/isa95-coverage.ts +22 -9
  279. package/server/service/twin-model/item-ref.ts +80 -0
  280. package/server/service/twin-model/name-index.ts +94 -0
  281. package/server/service/twin-model/project-structure.ts +1 -1
  282. package/server/service/twin-model/status-tally.ts +37 -0
  283. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  284. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  285. package/server/service/twin-model/twin-model-query.ts +156 -5
  286. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  287. package/server/service/twin-readiness/index.ts +3 -0
  288. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  289. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  290. package/shared/entity-delta.ts +31 -7
  291. package/shared/kpi-broadcast.ts +1 -1
  292. package/shared/touched-items.ts +73 -0
  293. package/test/adopt-structure-live.test.ts +7 -7
  294. package/test/axis-journal-evidence.test.ts +71 -0
  295. package/test/axis-read.test.ts +101 -1
  296. package/test/boot-resume.test.ts +93 -27
  297. package/test/broadcast-cost-baseline.test.ts +200 -0
  298. package/test/broadcast-period.test.ts +58 -0
  299. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  300. package/test/canonical-quantity-door.test.ts +61 -0
  301. package/test/command-routing.test.ts +1 -1
  302. package/test/control-capability.test.ts +103 -0
  303. package/test/declared-location-types.test.ts +89 -0
  304. package/test/declared-stimulus.test.ts +88 -0
  305. package/test/discovery-result.test.ts +1 -1
  306. package/test/duration-estimators.test.ts +1 -1
  307. package/test/entity-delta.test.ts +2 -2
  308. package/test/ingest-bench.test.ts +3 -3
  309. package/test/ingest-health-engine.test.ts +248 -0
  310. package/test/ingest-health-wiring.test.ts +119 -0
  311. package/test/ingest-health.test.ts +306 -0
  312. package/test/ingest-history.test.ts +247 -0
  313. package/test/ingest-running-guard.test.ts +5 -5
  314. package/test/instance-cache-lifecycle.test.ts +1 -1
  315. package/test/integration-probes.test.ts +103 -0
  316. package/test/integration-runner.test.ts +95 -0
  317. package/test/item-ref.test.ts +78 -0
  318. package/test/journal-read-discipline.test.ts +177 -0
  319. package/test/journal-retention.test.ts +133 -0
  320. package/test/journal-sort-axis.test.ts +142 -0
  321. package/test/journal-write-door.test.ts +110 -0
  322. package/test/journal-write-trend.test.ts +115 -0
  323. package/test/kernel-kind-guard.test.ts +4 -4
  324. package/test/kpi-baseline-db.test.ts +1 -1
  325. package/test/kpi-fold.test.ts +90 -3
  326. package/test/kpi-query-bench.test.ts +3 -3
  327. package/test/lineage-survives-restart.test.ts +20 -4
  328. package/test/live-cursor-wiring.test.ts +87 -0
  329. package/test/live-feed-registry.test.ts +15 -7
  330. package/test/live-kernel-facts.test.ts +7 -7
  331. package/test/live-mirror-parity.test.ts +6 -0
  332. package/test/load-meter.test.ts +29 -15
  333. package/test/local-declarations.test.ts +109 -2
  334. package/test/log-stamp.test.ts +59 -0
  335. package/test/loop-lag.test.ts +82 -0
  336. package/test/measured-yield.test.ts +90 -0
  337. package/test/model-gap.test.ts +153 -0
  338. package/test/oee-accumulator.test.ts +4 -0
  339. package/test/operation-basis.test.ts +28 -1
  340. package/test/operational-vocabulary.test.ts +108 -0
  341. package/test/operations-capability-db.test.ts +4 -4
  342. package/test/project-structure-db.test.ts +1 -1
  343. package/test/projection-reaches-screen.test.ts +1 -1
  344. package/test/property-effects.test.ts +28 -0
  345. package/test/read-failure-visible.test.ts +145 -0
  346. package/test/reference-grounding.test.ts +70 -0
  347. package/test/resolve-ts-siblings.mjs +52 -0
  348. package/test/restart-policy.test.ts +111 -0
  349. package/test/resync-origin-site.test.ts +7 -1
  350. package/test/revision-axis.test.ts +93 -0
  351. package/test/runtime-key.test.ts +2 -2
  352. package/test/scale-twin-bench.test.ts +2 -2
  353. package/test/source-outcome-audit.test.ts +104 -0
  354. package/test/spec-coverage.test.ts +1 -1
  355. package/test/stage-path.test.ts +95 -0
  356. package/test/standard-coverage.test.ts +15 -3
  357. package/test/status-tally.test.ts +55 -0
  358. package/test/structure-revision-db.test.ts +31 -27
  359. package/test/tenant-registry-db.test.ts +3 -3
  360. package/test/time-range.test.ts +152 -0
  361. package/test/touched-items.test.ts +61 -0
  362. package/test/twin-event-keys.test.ts +10 -2
  363. package/test/twin-model-item-db.test.ts +5 -3
  364. package/test/twin-model-tree-db.test.ts +7 -7
  365. package/test/twin-origin-resync.test.ts +4 -4
  366. package/test/warm-start-seam.test.ts +4 -0
  367. package/test/yield-loop.test.ts +199 -0
  368. package/tsconfig.shared.tsbuildinfo +1 -1
  369. package/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"live-feed-registry.js","sourceRoot":"","sources":["../../server/engine/live-feed-registry.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAQH,sDAEC;AAQD,4CASC;AAYD,0CAIC;AAtCD,MAAM,MAAM,GAAoB,EAAE,CAAA;AAElC,gCAAgC;AAChC,SAAgB,qBAAqB,CAAC,KAAoB;IACxD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,UAAkB;IACjD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,OAAO,IAAI,CAAA;QAC/C,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CAAC,gDAAgD,EAAE,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAA;QACjF,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAYD,SAAgB,eAAe,CAAC,IAAqE;IACnG,IAAI,IAAI,CAAC,WAAW,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAA;IAC1D,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,gBAAgB,CAAA;IAC1C,OAAO,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAA;AACpE,CAAC","sourcesContent":["/*\n * 「이 트윈에 계측이 들어오고 있나」 — **그 사실을 한 자리에서 묻는다.**\n *\n * ── 무엇이 보이지 않았나 (2026-08-14) ───────────────────────────────────────\n * 미러 트윈은 커널이 돌기만 하면 「도는 중」이라 보고했다. 그런데 계측을 나르는 피드가 떨어져 있으면\n * 그 트윈은 **아무것도 받지 않으면서 도는 중**이다 — 화면에서 두 상태가 똑같이 보였고, 사람은\n * 「값이 안 변하네」로 뒤늦게 알게 됐다(재기동 뒤 실제로 그랬다).\n *\n * ── 왜 장부가 하나여야 하나 ────────────────────────────────────────────────\n * 피드를 여는 곳이 둘이다: 커넥터 기반(`reference-live`)과 프리셋 기반(앱의 `provision-from-preset`).\n * 한쪽만 보고 「끊김」이라 말하면 **멀쩡히 흐르는 트윈을 끊겼다고 부르게 된다** — 거짓 경보는 참 경보를\n * 죽인다. 그래서 각 계층이 자기 장부를 여기 **등록**하고, 묻는 쪽은 이 한 자리에만 묻는다.\n *\n * 호스트는 어떤 어댑터·프리셋이 있는지 몰라도 된다(계층을 거꾸로 잇지 않는다).\n */\n\n/** 지금 피드가 붙어 있는 인스턴스 id 들 — 그 계층의 장부가 답한다. */\nexport type LiveFeedProbe = () => string[]\n\nconst probes: LiveFeedProbe[] = []\n\n/** 피드 장부를 등록한다(모듈 로드 시 한 번). */\nexport function registerLiveFeedProbe(probe: LiveFeedProbe): void {\n probes.push(probe)\n}\n\n/**\n * 피드가 붙어 있나 — 어느 장부든 그렇다고 하면 그렇다.\n *\n * 장부가 터지는 것을 이 판정 전체의 실패로 만들지 않는다: 한 계층이 답하지 못해도 다른 계층의 사실은\n * 그대로 참이다. 다만 **조용히 넘기지 않는다** — 답하지 못한 장부는 로그에 남긴다.\n */\nexport function liveFeedAttached(instanceId: string): boolean {\n for (const probe of probes) {\n try {\n if (probe().includes(instanceId)) return true\n } catch (e: any) {\n console.warn('[twin-live] a feed registry could not answer —', e?.message ?? e)\n }\n }\n return false\n}\n\n/**\n * 이 트윈의 **피드 사실** — 화면이 그대로 말할 수 있는 세 값.\n *\n * `not-applicable` 을 `detached` 와 섞지 않는다(이 시스템이 내내 지켜 온 「없음 ≠ 해당 없음」):\n * · 시뮬 트윈에는 나를 피드가 **없어야 한다** — 그건 결함이 아니라 그 트윈의 성질이다.\n * · 멈춘 트윈에게 피드를 묻는 것은 뜻이 없다 — 사람이 멈춰 둔 것이다.\n * · 남는 것 하나가 우리가 보이려던 사실이다: **도는 미러인데 아무것도 오지 않는다.**\n */\nexport type LiveFeedState = 'attached' | 'detached' | 'not-applicable'\n\nexport function liveFeedStateOf(args: { realityMode?: string; running?: boolean; instanceId: string }): LiveFeedState {\n if (args.realityMode !== 'mirror') return 'not-applicable'\n if (!args.running) return 'not-applicable'\n return liveFeedAttached(args.instanceId) ? 'attached' : 'detached'\n}\n"]}
1
+ {"version":3,"file":"live-feed-registry.js","sourceRoot":"","sources":["../../server/engine/live-feed-registry.ts"],"names":[],"mappings":";;AAuBA,sDAEC;AAQD,4CASC;AAYD,0CAIC;AA1DD,qCAAmC;AAoBnC,MAAM,MAAM,GAAoB,EAAE,CAAA;AAElC,gCAAgC;AAChC,SAAgB,qBAAqB,CAAC,KAAoB;IACxD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,gBAAgB,CAAC,UAAkB;IACjD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,IAAI,KAAK,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC;gBAAE,OAAO,IAAI,CAAA;QAC/C,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YAChB,IAAA,iBAAQ,EAAC,gDAAgD,EAAE,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAA;QAC7E,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAYD,SAAgB,eAAe,CAAC,IAAuE;IACrG,IAAI,IAAI,CAAC,aAAa,KAAK,QAAQ;QAAE,OAAO,gBAAgB,CAAA;IAC5D,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,gBAAgB,CAAA;IAC1C,OAAO,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAA;AACpE,CAAC","sourcesContent":["import { twinWarn } from './log.js'\n/*\n * 「이 트윈에 계측이 들어오고 있나」 — **그 사실을 한 자리에서 묻는다.**\n *\n * ── 무엇이 보이지 않았나 (2026-08-14) ───────────────────────────────────────\n * 미러 트윈은 커널이 돌기만 하면 「도는 중」이라 보고했다. 그런데 계측을 나르는 피드가 떨어져 있으면\n * 그 트윈은 **아무것도 받지 않으면서 도는 중**이다 — 화면에서 두 상태가 똑같이 보였고, 사람은\n * 「값이 안 변하네」로 뒤늦게 알게 됐다(재기동 뒤 실제로 그랬다).\n *\n * ── 왜 장부가 하나여야 하나 ────────────────────────────────────────────────\n * 피드를 여는 곳이 둘이다: 커넥터 기반(`reference-live`)과 프리셋 기반(앱의 `provision-from-preset`).\n * 한쪽만 보고 「끊김」이라 말하면 **멀쩡히 흐르는 트윈을 끊겼다고 부르게 된다** — 거짓 경보는 참 경보를\n * 죽인다. 그래서 각 계층이 자기 장부를 여기 **등록**하고, 묻는 쪽은 이 한 자리에만 묻는다.\n *\n * 호스트는 어떤 어댑터·프리셋이 있는지 몰라도 된다(계층을 거꾸로 잇지 않는다).\n */\n\n/** 지금 피드가 붙어 있는 인스턴스 id 들 — 그 계층의 장부가 답한다. */\nexport type LiveFeedProbe = () => string[]\n\nconst probes: LiveFeedProbe[] = []\n\n/** 피드 장부를 등록한다(모듈 로드 시 한 번). */\nexport function registerLiveFeedProbe(probe: LiveFeedProbe): void {\n probes.push(probe)\n}\n\n/**\n * 피드가 붙어 있나 — 어느 장부든 그렇다고 하면 그렇다.\n *\n * 장부가 터지는 것을 이 판정 전체의 실패로 만들지 않는다: 한 계층이 답하지 못해도 다른 계층의 사실은\n * 그대로 참이다. 다만 **조용히 넘기지 않는다** — 답하지 못한 장부는 로그에 남긴다.\n */\nexport function liveFeedAttached(instanceId: string): boolean {\n for (const probe of probes) {\n try {\n if (probe().includes(instanceId)) return true\n } catch (e: any) {\n twinWarn('[twin-live] a feed registry could not answer —', e?.message ?? e)\n }\n }\n return false\n}\n\n/**\n * 이 트윈의 **피드 사실** — 화면이 그대로 말할 수 있는 세 값.\n *\n * `not-applicable` 을 `detached` 와 섞지 않는다(이 시스템이 내내 지켜 온 「없음 ≠ 해당 없음」):\n * · 시뮬 트윈에는 나를 피드가 **없어야 한다** — 그건 결함이 아니라 그 트윈의 성질이다.\n * · 멈춘 트윈에게 피드를 묻는 것은 뜻이 없다 — 사람이 멈춰 둔 것이다.\n * · 남는 것 하나가 우리가 보이려던 사실이다: **도는 미러인데 아무것도 오지 않는다.**\n */\nexport type LiveFeedState = 'attached' | 'detached' | 'not-applicable'\n\nexport function liveFeedStateOf(args: { restartPolicy?: string; running?: boolean; instanceId: string }): LiveFeedState {\n if (args.restartPolicy !== 'resync') return 'not-applicable'\n if (!args.running) return 'not-applicable'\n return liveFeedAttached(args.instanceId) ? 'attached' : 'detached'\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  /** 작업 이름 — 새 작업을 재기 시작하면 여기 늘린다(문자열을 자유롭게 쓰지 않는다: 오타가 새 칸이 된다). */
2
- export declare const LOAD_PHASES: readonly ["tick", "snapshot", "deltas", "publish", "journal", "ingest", "forecast", "forkTick"];
2
+ export declare const LOAD_PHASES: readonly ["tick", "snapshot", "deltas", "publish", "journal", "ingest", "forecast", "forkTick", "warmStart", "stimulus"];
3
3
  export type LoadPhase = (typeof LOAD_PHASES)[number];
4
4
  export interface PhaseStats {
5
5
  /** 잰 횟수. */
@@ -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"]}
@@ -109,12 +109,6 @@ export interface DeclarablePropertySpec {
109
109
  */
110
110
  targets?: string[];
111
111
  }
112
- /**
113
- * 값 + 단위 → **ISO 8601 기간** — 커널이 읽는 표기로 바꾼다(커널은 초·분을 모른다, 표준 표기만 안다).
114
- *
115
- * 읽을 수 없으면 `undefined` — 부르는 쪽이 그 사실을 거절 사유로 올린다(0 을 만들지 않는다: 소요 0 인
116
- * 작업은 영원히 끝나지 않는다).
117
- */
118
112
  export declare function isoDurationOf(value: string | number, uom: string): string | undefined;
119
113
  /**
120
114
  * 이 종류의 트윈에 **선언할 수 있는 속성 목록** — 화면이 도메인의 모양을 알지 않게.
@@ -161,6 +155,7 @@ export declare function applyLocalDeclarations<T extends {
161
155
  locations?: any[];
162
156
  equipment?: any[];
163
157
  localDurations?: Record<string, string>;
158
+ localParams?: Record<string, Record<string, string>>;
164
159
  }>(model: T, declarations: readonly LocalDeclaration[] | undefined | null): {
165
160
  model: T;
166
161
  missingTargets: {
@@ -215,6 +210,7 @@ export declare function restoreRevoked<T extends {
215
210
  locations?: any[];
216
211
  equipment?: any[];
217
212
  localDurations?: Record<string, string>;
213
+ localParams?: Record<string, Record<string, string>>;
218
214
  }>(model: T, revoked: readonly RevokedDeclaration[]): {
219
215
  model: T;
220
216
  restored: RevokedDeclaration[];
@@ -241,6 +237,7 @@ export declare function applyDeclarationLayers<T extends {
241
237
  locations?: any[];
242
238
  equipment?: any[];
243
239
  localDurations?: Record<string, string>;
240
+ localParams?: Record<string, Record<string, string>>;
244
241
  }>(model: T, spaceLayer: readonly LocalDeclaration[] | undefined | null, instanceLayer: readonly LocalDeclaration[] | undefined | null): {
245
242
  model: T;
246
243
  /** 현장 겹을 얹은 결과 — 저장해야 기억이 낡지 않는다. */
@@ -129,6 +129,28 @@ const COMMON_SPECS = [
129
129
  range: [0.01, 100000],
130
130
  note: 'how long this operation takes at this site — the simulation and the forecast use it instead of the built-in constant. a unit is required. history still wins where it exists.',
131
131
  uomChoices: Object.keys(DURATION_UOM_MS)
132
+ },
133
+ /*
134
+ * 양품률 — **퍼센트로 받는다**(80 이 80%다).
135
+ *
136
+ * 커널이 읽는 값은 0..1 이지만(ISA-95 `Parameter` 값 그대로) 사람에게 0.8 을 넣으라고 하면 SOC 에서
137
+ * 겪은 함정을 되풀게 된다(0.8 을 0.8% 로 쓴 일이 있다). 그래서 화면·AI 는 퍼센트로 말하고, 비율로
138
+ * 바꾸는 일은 얹는 자리 한 곳에서 한다.
139
+ */
140
+ {
141
+ id: twin_kernel_1.OPERATION_PROPERTY.yield,
142
+ uom: '%',
143
+ dataType: 'xs:double',
144
+ range: [1, 100],
145
+ note: 'good-output rate of this operation as a percentage 1-100 (95 means 95%) — the kernel reads it as a ratio, and this value replaces the built-in default that also drives the high-scrap attention.'
146
+ },
147
+ /* 셋업·체인지오버 — 가동 앞에 붙는 시간. 소요시간과 같은 단위 목록을 쓴다(같은 종류의 값이다). */
148
+ {
149
+ id: twin_kernel_1.OPERATION_PROPERTY.setupDuration,
150
+ dataType: 'xs:double',
151
+ range: [0.01, 100000],
152
+ note: 'changeover time before this operation runs — added in front of the cycle. a unit is required.',
153
+ uomChoices: Object.keys(DURATION_UOM_MS)
132
154
  }
133
155
  ];
134
156
  /**
@@ -137,6 +159,17 @@ const COMMON_SPECS = [
137
159
  * 읽을 수 없으면 `undefined` — 부르는 쪽이 그 사실을 거절 사유로 올린다(0 을 만들지 않는다: 소요 0 인
138
160
  * 작업은 영원히 끝나지 않는다).
139
161
  */
162
+ /**
163
+ * 공정 모수 표에 한 칸 담는다 — **커널이 읽는 표기 그대로**(수율 0..1, 셋업 ISO 8601).
164
+ *
165
+ * 표를 통째로 새로 만들어 얹는다: 같은 모델 객체를 여러 겹이 지나므로(현장 → 트윈) 그때마다 새 객체여야
166
+ * 앞 겹의 것을 조용히 덮어쓰지 않는다.
167
+ */
168
+ function putParam(next, kind, paramId, value) {
169
+ const table = { ...(next.localParams ?? {}) };
170
+ table[kind] = { ...(table[kind] ?? {}), [paramId]: value };
171
+ next.localParams = table;
172
+ }
140
173
  function isoDurationOf(value, uom) {
141
174
  const n = Number(value);
142
175
  const per = DURATION_UOM_MS[String(uom ?? '').trim()];
@@ -351,20 +384,46 @@ function applyLocalDeclarations(model, declarations) {
351
384
  * 이 표는 **전부 우리가 정한 값**이다(원천이 그리는 자리가 아니다). 그래서 기억(`replaced`)은 언제나
352
385
  * `null` 이고, 철회는 「표에서 뺀다」로 끝난다 — 그러면 원천 명세나 커널 상수가 다시 답한다.
353
386
  */
354
- const putOperation = (durations, d, memo) => {
387
+ const putOperation = (next, d, memo) => {
355
388
  for (let i = 0; i < d.properties.length; i++) {
356
389
  const p = d.properties[i];
357
- if (p.id !== twin_kernel_1.OPERATION_PROPERTY.duration) {
358
- unreadable.push({ target: 'operation', id: d.id, propertyId: p.id });
390
+ const uom = String(p.uom ?? '');
391
+ /*
392
+ * 값을 **커널이 읽는 표기로** 바꿔 담는다 — 시간은 ISO 8601, 수율은 비율(0..1).
393
+ * 바꿀 수 없으면 담지 않고 그 사실을 낸다(조용히 0 이나 빈 값으로 만들지 않는다).
394
+ */
395
+ if (p.id === twin_kernel_1.OPERATION_PROPERTY.duration) {
396
+ const iso = isoDurationOf(p.value, uom);
397
+ if (!iso) {
398
+ unreadable.push({ target: 'operation', id: d.id, propertyId: p.id });
399
+ continue;
400
+ }
401
+ next.localDurations = { ...(next.localDurations ?? {}), [d.id]: iso };
402
+ memo.properties[i].replaced = null;
403
+ continue;
404
+ }
405
+ if (p.id === twin_kernel_1.OPERATION_PROPERTY.setupDuration) {
406
+ const iso = isoDurationOf(p.value, uom);
407
+ if (!iso) {
408
+ unreadable.push({ target: 'operation', id: d.id, propertyId: p.id });
409
+ continue;
410
+ }
411
+ putParam(next, d.id, twin_kernel_1.OP_PARAM.setupDuration, iso);
412
+ memo.properties[i].replaced = null;
359
413
  continue;
360
414
  }
361
- const iso = isoDurationOf(p.value, String(p.uom ?? ''));
362
- if (!iso) {
363
- unreadable.push({ target: 'operation', id: d.id, propertyId: p.id });
415
+ if (p.id === twin_kernel_1.OPERATION_PROPERTY.yield) {
416
+ const pct = Number(p.value);
417
+ if (!Number.isFinite(pct) || pct <= 0 || pct > 100) {
418
+ unreadable.push({ target: 'operation', id: d.id, propertyId: p.id });
419
+ continue;
420
+ }
421
+ /* 퍼센트 → 비율. 이 환산이 있는 자리는 여기 하나다(두 곳에서 나누면 100 배가 두 번 걸린다). */
422
+ putParam(next, d.id, twin_kernel_1.OP_PARAM.yield, String(pct / 100));
423
+ memo.properties[i].replaced = null;
364
424
  continue;
365
425
  }
366
- durations[d.id] = iso;
367
- memo.properties[i].replaced = null;
426
+ unreadable.push({ target: 'operation', id: d.id, propertyId: p.id });
368
427
  }
369
428
  };
370
429
  /* 얕은 복사로는 안 된다 — 자리·설비 항목을 손대므로 그 배열과 항목까지 새로 만든다. */
@@ -372,16 +431,15 @@ function applyLocalDeclarations(model, declarations) {
372
431
  ...model,
373
432
  ...(model.locations ? { locations: model.locations.map(n => ({ ...n, ...(n?.properties ? { properties: [...n.properties] } : {}) })) } : {}),
374
433
  ...(model.equipment ? { equipment: model.equipment.map(e => ({ ...e, ...(e?.properties ? { properties: [...e.properties] } : {}) })) } : {}),
375
- ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {})
434
+ ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {}),
435
+ ...(model.localParams ? { localParams: Object.fromEntries(Object.entries(model.localParams).map(([k, v]) => [k, { ...v }])) } : {})
376
436
  };
377
437
  for (let i = 0; i < decls.length; i++) {
378
438
  const d = decls[i];
379
439
  if (d.target === 'operation') {
380
- /* 빈 표를 남기지 않는다 — 「선언이 있다」와 「읽을 수 있는 선언이 하나도 없다」는 다른 사실이다. */
381
- const table = next.localDurations ?? {};
382
- putOperation(table, d, layer[i]);
383
- if (Object.keys(table).length)
384
- next.localDurations = table;
440
+ /* 빈 표를 남기지 않는다 — 「선언이 있다」와 「읽을 수 있는 선언이 하나도 없다」는 다른 사실이다
441
+ (`putOperation` 담을 것이 있을 때만 표를 만든다). */
442
+ putOperation(next, d, layer[i]);
385
443
  continue;
386
444
  }
387
445
  const ok = put(d.target === 'equipment' ? next.equipment : next.locations, d, layer[i]);
@@ -436,7 +494,8 @@ function restoreRevoked(model, revoked) {
436
494
  ...model,
437
495
  ...(model.locations ? { locations: model.locations.map(n => ({ ...n, ...(n?.properties ? { properties: [...n.properties] } : {}) })) } : {}),
438
496
  ...(model.equipment ? { equipment: model.equipment.map(e => ({ ...e, ...(e?.properties ? { properties: [...e.properties] } : {}) })) } : {}),
439
- ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {})
497
+ ...(model.localDurations ? { localDurations: { ...model.localDurations } } : {}),
498
+ ...(model.localParams ? { localParams: Object.fromEntries(Object.entries(model.localParams).map(([k, v]) => [k, { ...v }])) } : {})
440
499
  };
441
500
  const restored = [];
442
501
  const unknownSource = [];
@@ -447,13 +506,35 @@ function restoreRevoked(model, revoked) {
447
506
  * 빼면 원천 명세(있으면)나 커널 상수가 다시 답한다: 그것이 「선언 이전」의 상태다.
448
507
  */
449
508
  if (r.target === 'operation') {
509
+ /* 시간은 시간 표에서, 모수(수율·셋업)는 모수 표에서 뺀다 — 담긴 자리에서 빼야 실제로 빠진다. */
510
+ const paramId = r.propertyId === twin_kernel_1.OPERATION_PROPERTY.yield
511
+ ? twin_kernel_1.OP_PARAM.yield
512
+ : r.propertyId === twin_kernel_1.OPERATION_PROPERTY.setupDuration
513
+ ? twin_kernel_1.OP_PARAM.setupDuration
514
+ : undefined;
515
+ if (paramId) {
516
+ const params = next.localParams ?? {};
517
+ if (!params[r.id] || !(paramId in params[r.id])) {
518
+ missingTargets.push({ target: r.target, id: r.id });
519
+ continue;
520
+ }
521
+ delete params[r.id][paramId];
522
+ /* 빈 껍데기를 남기지 않는다 — 「선언이 있다」와 「빈 표가 있다」가 구별되지 않는다. */
523
+ if (!Object.keys(params[r.id]).length)
524
+ delete params[r.id];
525
+ if (!Object.keys(params).length)
526
+ delete next.localParams;
527
+ else
528
+ next.localParams = params;
529
+ restored.push(r);
530
+ continue;
531
+ }
450
532
  const table = next.localDurations ?? {};
451
533
  if (!(r.id in table)) {
452
534
  missingTargets.push({ target: r.target, id: r.id });
453
535
  continue;
454
536
  }
455
537
  delete table[r.id];
456
- /* 빈 표는 남기지 않는다 — 「선언이 있다」와 「빈 표가 있다」가 구별되지 않는다. */
457
538
  if (!Object.keys(table).length)
458
539
  delete next.localDurations;
459
540
  else