@things-factory/headless-twin 10.0.15 → 10.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. package/dist-server/engine/attention-digest.d.ts +4 -4
  2. package/dist-server/engine/attention-digest.js +7 -7
  3. package/dist-server/engine/attention-digest.js.map +1 -1
  4. package/dist-server/engine/canonical-ingest.d.ts +18 -82
  5. package/dist-server/engine/canonical-ingest.js +183 -18
  6. package/dist-server/engine/canonical-ingest.js.map +1 -1
  7. package/dist-server/engine/command-routing.js +1 -1
  8. package/dist-server/engine/command-routing.js.map +1 -1
  9. package/dist-server/engine/declared-stimulus.js +1 -1
  10. package/dist-server/engine/declared-stimulus.js.map +1 -1
  11. package/dist-server/engine/energy-topology.js +4 -4
  12. package/dist-server/engine/energy-topology.js.map +1 -1
  13. package/dist-server/engine/ingest-dedupe.d.ts +38 -0
  14. package/dist-server/engine/ingest-dedupe.js +145 -0
  15. package/dist-server/engine/ingest-dedupe.js.map +1 -0
  16. package/dist-server/engine/ingest-health.d.ts +90 -10
  17. package/dist-server/engine/ingest-health.js +125 -16
  18. package/dist-server/engine/ingest-health.js.map +1 -1
  19. package/dist-server/engine/integration-probes.d.ts +3 -3
  20. package/dist-server/engine/integration-probes.js +6 -6
  21. package/dist-server/engine/integration-probes.js.map +1 -1
  22. package/dist-server/engine/kpi-baseline.js +4 -4
  23. package/dist-server/engine/kpi-baseline.js.map +1 -1
  24. package/dist-server/engine/kpi-fold.d.ts +212 -14
  25. package/dist-server/engine/kpi-fold.js +374 -21
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.d.ts +11 -11
  28. package/dist-server/engine/kpi-query.js +281 -36
  29. package/dist-server/engine/kpi-query.js.map +1 -1
  30. package/dist-server/engine/live-feed-registry.d.ts +1 -1
  31. package/dist-server/engine/live-feed-registry.js +1 -1
  32. package/dist-server/engine/live-feed-registry.js.map +1 -1
  33. package/dist-server/engine/load-meter.d.ts +1 -1
  34. package/dist-server/engine/load-meter.js.map +1 -1
  35. package/dist-server/engine/local-declarations.d.ts +7 -7
  36. package/dist-server/engine/local-declarations.js +15 -14
  37. package/dist-server/engine/local-declarations.js.map +1 -1
  38. package/dist-server/engine/log.d.ts +1 -1
  39. package/dist-server/engine/log.js +1 -1
  40. package/dist-server/engine/log.js.map +1 -1
  41. package/dist-server/engine/measured-estimator.js +1 -1
  42. package/dist-server/engine/measured-estimator.js.map +1 -1
  43. package/dist-server/engine/measured-yield.js +1 -1
  44. package/dist-server/engine/measured-yield.js.map +1 -1
  45. package/dist-server/engine/model-basis.d.ts +1 -1
  46. package/dist-server/engine/model-basis.js +1 -1
  47. package/dist-server/engine/model-basis.js.map +1 -1
  48. package/dist-server/engine/model-gap.d.ts +3 -3
  49. package/dist-server/engine/model-gap.js +1 -1
  50. package/dist-server/engine/model-gap.js.map +1 -1
  51. package/dist-server/engine/model-vocabulary.js +3 -3
  52. package/dist-server/engine/model-vocabulary.js.map +1 -1
  53. package/dist-server/engine/oee-accumulator.d.ts +6 -5
  54. package/dist-server/engine/oee-accumulator.js +6 -5
  55. package/dist-server/engine/oee-accumulator.js.map +1 -1
  56. package/dist-server/engine/property-effects.js +2 -2
  57. package/dist-server/engine/property-effects.js.map +1 -1
  58. package/dist-server/engine/read-time.d.ts +22 -0
  59. package/dist-server/engine/read-time.js +81 -0
  60. package/dist-server/engine/read-time.js.map +1 -0
  61. package/dist-server/engine/restart-policy.js +1 -1
  62. package/dist-server/engine/restart-policy.js.map +1 -1
  63. package/dist-server/engine/runtime-key.js +1 -1
  64. package/dist-server/engine/runtime-key.js.map +1 -1
  65. package/dist-server/engine/spec-coverage.d.ts +2 -2
  66. package/dist-server/engine/spec-coverage.js +1 -1
  67. package/dist-server/engine/spec-coverage.js.map +1 -1
  68. package/dist-server/engine/stage-path.d.ts +4 -4
  69. package/dist-server/engine/stage-path.js +1 -1
  70. package/dist-server/engine/stage-path.js.map +1 -1
  71. package/dist-server/engine/state-axes.d.ts +1 -1
  72. package/dist-server/engine/state-axes.js +2 -2
  73. package/dist-server/engine/state-axes.js.map +1 -1
  74. package/dist-server/engine/travel-estimator.d.ts +2 -2
  75. package/dist-server/engine/travel-estimator.js +1 -1
  76. package/dist-server/engine/travel-estimator.js.map +1 -1
  77. package/dist-server/engine/twin-engine.d.ts +307 -54
  78. package/dist-server/engine/twin-engine.js +822 -196
  79. package/dist-server/engine/twin-engine.js.map +1 -1
  80. package/dist-server/engine/warm-start.d.ts +2 -2
  81. package/dist-server/engine/warm-start.js +5 -5
  82. package/dist-server/engine/warm-start.js.map +1 -1
  83. package/dist-server/index.js +1 -1
  84. package/dist-server/index.js.map +1 -1
  85. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js +1 -1
  86. package/dist-server/migrations/1786000000000-RenameTwinInstanceBoardToModel.js.map +1 -1
  87. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +3 -3
  88. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -1
  89. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +2 -2
  90. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -1
  91. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.d.ts +5 -0
  92. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js +54 -0
  93. package/dist-server/migrations/1786300000000-IndexEventTypeByTime.js.map +1 -0
  94. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.d.ts +5 -0
  95. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js +44 -0
  96. package/dist-server/migrations/1786400000000-IndexEventCreatedAt.js.map +1 -0
  97. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.d.ts +6 -0
  98. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js +84 -0
  99. package/dist-server/migrations/1786500000000-CreateTwinSubjectEvents.js.map +1 -0
  100. package/dist-server/migrations/index.js +7 -1
  101. package/dist-server/migrations/index.js.map +1 -1
  102. package/dist-server/routes.d.ts +1 -0
  103. package/dist-server/routes.js +69 -5
  104. package/dist-server/routes.js.map +1 -1
  105. package/dist-server/service/index.d.ts +2 -2
  106. package/dist-server/service/index.js +30 -24
  107. package/dist-server/service/index.js.map +1 -1
  108. package/dist-server/service/reference/control-routing.d.ts +1 -1
  109. package/dist-server/service/reference/control-routing.js +1 -1
  110. package/dist-server/service/reference/control-routing.js.map +1 -1
  111. package/dist-server/service/reference/discovery-result.d.ts +2 -2
  112. package/dist-server/service/reference/discovery-result.js +3 -3
  113. package/dist-server/service/reference/discovery-result.js.map +1 -1
  114. package/dist-server/service/reference/hook-contract.d.ts +43 -0
  115. package/dist-server/service/reference/hook-contract.js +59 -0
  116. package/dist-server/service/reference/hook-contract.js.map +1 -0
  117. package/dist-server/service/reference/ingest-space.d.ts +1 -1
  118. package/dist-server/service/reference/ingest-space.js +4 -4
  119. package/dist-server/service/reference/ingest-space.js.map +1 -1
  120. package/dist-server/service/reference/knob-defaults.d.ts +1 -1
  121. package/dist-server/service/reference/knob-defaults.js +4 -4
  122. package/dist-server/service/reference/knob-defaults.js.map +1 -1
  123. package/dist-server/service/reference/reference-adapter.d.ts +116 -9
  124. package/dist-server/service/reference/reference-adapter.js +27 -0
  125. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  126. package/dist-server/service/reference/reference-hook.d.ts +20 -0
  127. package/dist-server/service/reference/reference-hook.js +133 -0
  128. package/dist-server/service/reference/reference-hook.js.map +1 -0
  129. package/dist-server/service/reference/reference-live.d.ts +2 -2
  130. package/dist-server/service/reference/reference-live.js +76 -13
  131. package/dist-server/service/reference/reference-live.js.map +1 -1
  132. package/dist-server/service/reference/reference-master.d.ts +25 -16
  133. package/dist-server/service/reference/reference-master.js +35 -13
  134. package/dist-server/service/reference/reference-master.js.map +1 -1
  135. package/dist-server/service/reference/reference-resolver.d.ts +1 -1
  136. package/dist-server/service/reference/reference-resolver.js +12 -12
  137. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  138. package/dist-server/service/reference/template-registry.d.ts +1 -1
  139. package/dist-server/service/reference/template-registry.js.map +1 -1
  140. package/dist-server/service/reference/twin-reference.d.ts +3 -3
  141. package/dist-server/service/reference/twin-reference.js.map +1 -1
  142. package/dist-server/service/twin-attention/twin-attention-query.d.ts +1 -1
  143. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  144. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  145. package/dist-server/service/twin-audit/command-audit.d.ts +1 -1
  146. package/dist-server/service/twin-audit/command-audit.js.map +1 -1
  147. package/dist-server/service/twin-audit/twin-audit-event.d.ts +1 -1
  148. package/dist-server/service/twin-audit/twin-audit-event.js.map +1 -1
  149. package/dist-server/service/twin-audit/twin-audit-query.js +1 -1
  150. package/dist-server/service/twin-audit/twin-audit-query.js.map +1 -1
  151. package/dist-server/service/twin-backfill/backfill-period-facts.d.ts +24 -0
  152. package/dist-server/service/twin-backfill/backfill-period-facts.js +96 -0
  153. package/dist-server/service/twin-backfill/backfill-period-facts.js.map +1 -0
  154. package/dist-server/service/twin-backfill/backfill-shape.d.ts +34 -0
  155. package/dist-server/service/twin-backfill/backfill-shape.js +75 -0
  156. package/dist-server/service/twin-backfill/backfill-shape.js.map +1 -0
  157. package/dist-server/service/twin-backfill/index.d.ts +2 -0
  158. package/dist-server/service/twin-backfill/index.js +6 -0
  159. package/dist-server/service/twin-backfill/index.js.map +1 -0
  160. package/dist-server/service/twin-backfill/twin-backfill-resolver.d.ts +10 -0
  161. package/dist-server/service/twin-backfill/twin-backfill-resolver.js +44 -0
  162. package/dist-server/service/twin-backfill/twin-backfill-resolver.js.map +1 -0
  163. package/dist-server/service/twin-control/twin-control-mutation.js +3 -3
  164. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  165. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  166. package/dist-server/service/twin-event/twin-event-keys.js +2 -2
  167. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  168. package/dist-server/service/twin-event/twin-event-type.d.ts +1 -0
  169. package/dist-server/service/twin-event/twin-event-type.js +8 -1
  170. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  171. package/dist-server/service/twin-event/twin-event.d.ts +2 -2
  172. package/dist-server/service/twin-event/twin-event.js +32 -4
  173. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  174. package/dist-server/service/twin-forecast/forecast-metrics.js +1 -1
  175. package/dist-server/service/twin-forecast/forecast-metrics.js.map +1 -1
  176. package/dist-server/service/twin-forecast/gap-analytics.d.ts +1 -1
  177. package/dist-server/service/twin-forecast/gap-analytics.js.map +1 -1
  178. package/dist-server/service/twin-forecast/twin-forecast-query.js +29 -11
  179. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  180. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +1 -1
  181. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +1 -1
  182. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -1
  183. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +1 -1
  184. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -1
  185. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  186. package/dist-server/service/twin-journal/event-type-split-shape.d.ts +50 -0
  187. package/dist-server/service/twin-journal/event-type-split-shape.js +111 -0
  188. package/dist-server/service/twin-journal/event-type-split-shape.js.map +1 -0
  189. package/dist-server/service/twin-journal/event-type-split.d.ts +15 -0
  190. package/dist-server/service/twin-journal/event-type-split.js +102 -0
  191. package/dist-server/service/twin-journal/event-type-split.js.map +1 -0
  192. package/dist-server/service/twin-journal/journal-order.d.ts +15 -0
  193. package/dist-server/service/twin-journal/journal-order.js +17 -0
  194. package/dist-server/service/twin-journal/journal-order.js.map +1 -0
  195. package/dist-server/service/twin-journal/twin-journal-query.d.ts +5 -5
  196. package/dist-server/service/twin-journal/twin-journal-query.js +232 -21
  197. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  198. package/dist-server/service/twin-lifecycle/domain-catalog.js +2 -1
  199. package/dist-server/service/twin-lifecycle/domain-catalog.js.map +1 -1
  200. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +27 -1
  201. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +59 -3
  202. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  203. package/dist-server/service/twin-metrics/twin-metrics-query.js +14 -1
  204. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  205. package/dist-server/service/twin-model/axis-journal-evidence.js +1 -1
  206. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -1
  207. package/dist-server/service/twin-model/epcis-coverage.js +34 -3
  208. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  209. package/dist-server/service/twin-model/iec61850-coverage.js +1 -1
  210. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  211. package/dist-server/service/twin-model/isa95-coverage.d.ts +12 -9
  212. package/dist-server/service/twin-model/isa95-coverage.js +33 -9
  213. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  214. package/dist-server/service/twin-model/item-ref.js +2 -2
  215. package/dist-server/service/twin-model/item-ref.js.map +1 -1
  216. package/dist-server/service/twin-model/name-index.d.ts +1 -1
  217. package/dist-server/service/twin-model/name-index.js +2 -2
  218. package/dist-server/service/twin-model/name-index.js.map +1 -1
  219. package/dist-server/service/twin-model/project-structure.d.ts +1 -1
  220. package/dist-server/service/twin-model/project-structure.js +7 -6
  221. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  222. package/dist-server/service/twin-model/twin-equipment.d.ts +2 -2
  223. package/dist-server/service/twin-model/twin-equipment.js +3 -3
  224. package/dist-server/service/twin-model/twin-equipment.js.map +1 -1
  225. package/dist-server/service/twin-model/twin-lineage-query.js +16 -5
  226. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  227. package/dist-server/service/twin-model/twin-location.d.ts +1 -1
  228. package/dist-server/service/twin-model/twin-location.js.map +1 -1
  229. package/dist-server/service/twin-model/twin-model-item-query.d.ts +3 -3
  230. package/dist-server/service/twin-model/twin-model-item-query.js +15 -15
  231. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  232. package/dist-server/service/twin-model/twin-model-mutation.d.ts +2 -2
  233. package/dist-server/service/twin-model/twin-model-mutation.js +4 -4
  234. package/dist-server/service/twin-model/twin-model-mutation.js.map +1 -1
  235. package/dist-server/service/twin-model/twin-model-query.js +51 -30
  236. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  237. package/dist-server/service/twin-model/twin-model-tree-query.js +23 -1
  238. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  239. package/dist-server/service/twin-model/twin-operation.d.ts +1 -1
  240. package/dist-server/service/twin-model/twin-operation.js +1 -1
  241. package/dist-server/service/twin-model/twin-operation.js.map +1 -1
  242. package/dist-server/service/twin-space/move-space.js +2 -2
  243. package/dist-server/service/twin-space/move-space.js.map +1 -1
  244. package/dist-server/service/twin-space/representation-role.d.ts +2 -0
  245. package/dist-server/service/twin-space/representation-role.js +18 -0
  246. package/dist-server/service/twin-space/representation-role.js.map +1 -0
  247. package/dist-server/service/twin-space/space-integrity.d.ts +1 -1
  248. package/dist-server/service/twin-space/space-integrity.js.map +1 -1
  249. package/dist-server/service/twin-space/twin-space-representation.d.ts +2 -0
  250. package/dist-server/service/twin-space/twin-space-representation.js +16 -0
  251. package/dist-server/service/twin-space/twin-space-representation.js.map +1 -1
  252. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  253. package/dist-server/service/twin-space/twin-space-resolver.js +37 -8
  254. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  255. package/dist-server/service/twin-structure/twin-structure.js +3 -3
  256. package/dist-server/service/twin-structure/twin-structure.js.map +1 -1
  257. package/dist-server/service/twin-subject/event-subjects.d.ts +43 -0
  258. package/dist-server/service/twin-subject/event-subjects.js +137 -0
  259. package/dist-server/service/twin-subject/event-subjects.js.map +1 -0
  260. package/dist-server/service/twin-subject/index.d.ts +9 -0
  261. package/dist-server/service/twin-subject/index.js +16 -0
  262. package/dist-server/service/twin-subject/index.js.map +1 -0
  263. package/dist-server/service/twin-subject/item-resolver.d.ts +10 -0
  264. package/dist-server/service/twin-subject/item-resolver.js +93 -0
  265. package/dist-server/service/twin-subject/item-resolver.js.map +1 -0
  266. package/dist-server/service/twin-subject/subject-query-shape.d.ts +22 -0
  267. package/dist-server/service/twin-subject/subject-query-shape.js +88 -0
  268. package/dist-server/service/twin-subject/subject-query-shape.js.map +1 -0
  269. package/dist-server/service/twin-subject/twin-subject-backfill.d.ts +23 -0
  270. package/dist-server/service/twin-subject/twin-subject-backfill.js +159 -0
  271. package/dist-server/service/twin-subject/twin-subject-backfill.js.map +1 -0
  272. package/dist-server/service/twin-subject/twin-subject-event.d.ts +14 -0
  273. package/dist-server/service/twin-subject/twin-subject-event.js +111 -0
  274. package/dist-server/service/twin-subject/twin-subject-event.js.map +1 -0
  275. package/dist-server/service/twin-subject/twin-subject-fast-path.d.ts +39 -0
  276. package/dist-server/service/twin-subject/twin-subject-fast-path.js +295 -0
  277. package/dist-server/service/twin-subject/twin-subject-fast-path.js.map +1 -0
  278. package/dist-server/service/twin-subject/twin-subject-query.d.ts +27 -0
  279. package/dist-server/service/twin-subject/twin-subject-query.js +232 -0
  280. package/dist-server/service/twin-subject/twin-subject-query.js.map +1 -0
  281. package/dist-server/service/twin-subject/twin-subject-writer.d.ts +27 -0
  282. package/dist-server/service/twin-subject/twin-subject-writer.js +166 -0
  283. package/dist-server/service/twin-subject/twin-subject-writer.js.map +1 -0
  284. package/dist-server/service/twin-target/twin-target-resolver.js +1 -1
  285. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -1
  286. package/dist-shared/entity-delta.d.ts +1 -1
  287. package/dist-shared/entity-delta.js +4 -4
  288. package/dist-shared/entity-delta.js.map +1 -1
  289. package/dist-shared/touched-items.js +2 -2
  290. package/dist-shared/touched-items.js.map +1 -1
  291. package/dist-shared/twin-level.js +1 -1
  292. package/dist-shared/twin-level.js.map +1 -1
  293. package/package.json +7 -6
  294. package/server/engine/attention-digest.ts +9 -9
  295. package/server/engine/canonical-ingest.ts +202 -87
  296. package/server/engine/command-routing.ts +1 -1
  297. package/server/engine/declared-stimulus.ts +1 -1
  298. package/server/engine/energy-topology.ts +2 -2
  299. package/server/engine/ingest-dedupe.ts +149 -0
  300. package/server/engine/ingest-health.ts +171 -22
  301. package/server/engine/integration-probes.ts +7 -7
  302. package/server/engine/kpi-baseline.ts +4 -4
  303. package/server/engine/kpi-fold.ts +592 -41
  304. package/server/engine/kpi-query.ts +307 -48
  305. package/server/engine/live-feed-registry.ts +1 -1
  306. package/server/engine/load-meter.ts +1 -1
  307. package/server/engine/local-declarations.ts +15 -14
  308. package/server/engine/log.ts +1 -1
  309. package/server/engine/measured-estimator.ts +1 -1
  310. package/server/engine/measured-yield.ts +1 -1
  311. package/server/engine/model-basis.ts +2 -2
  312. package/server/engine/model-gap.ts +3 -3
  313. package/server/engine/model-vocabulary.ts +2 -2
  314. package/server/engine/oee-accumulator.ts +8 -6
  315. package/server/engine/property-effects.ts +2 -2
  316. package/server/engine/read-time.ts +77 -0
  317. package/server/engine/restart-policy.ts +1 -1
  318. package/server/engine/runtime-key.ts +1 -1
  319. package/server/engine/spec-coverage.ts +3 -3
  320. package/server/engine/stage-path.ts +5 -5
  321. package/server/engine/state-axes.ts +2 -2
  322. package/server/engine/travel-estimator.ts +2 -2
  323. package/server/engine/twin-engine.ts +906 -205
  324. package/server/engine/warm-start.ts +6 -6
  325. package/server/index.ts +1 -1
  326. package/server/migrations/1786000000000-RenameTwinInstanceBoardToModel.ts +1 -1
  327. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +3 -3
  328. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +2 -2
  329. package/server/migrations/1786300000000-IndexEventTypeByTime.ts +48 -0
  330. package/server/migrations/1786400000000-IndexEventCreatedAt.ts +38 -0
  331. package/server/migrations/1786500000000-CreateTwinSubjectEvents.ts +93 -0
  332. package/server/migrations/index.ts +7 -1
  333. package/server/routes.ts +73 -5
  334. package/server/service/index.ts +7 -1
  335. package/server/service/reference/control-routing.ts +1 -1
  336. package/server/service/reference/discovery-result.ts +3 -3
  337. package/server/service/reference/hook-contract.ts +103 -0
  338. package/server/service/reference/ingest-space.ts +4 -4
  339. package/server/service/reference/knob-defaults.ts +4 -4
  340. package/server/service/reference/reference-adapter.ts +145 -10
  341. package/server/service/reference/reference-hook.ts +145 -0
  342. package/server/service/reference/reference-live.ts +87 -15
  343. package/server/service/reference/reference-master.ts +51 -20
  344. package/server/service/reference/reference-resolver.ts +11 -11
  345. package/server/service/reference/template-registry.ts +1 -1
  346. package/server/service/reference/twin-reference.ts +3 -3
  347. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  348. package/server/service/twin-audit/command-audit.ts +1 -1
  349. package/server/service/twin-audit/twin-audit-event.ts +2 -2
  350. package/server/service/twin-audit/twin-audit-query.ts +1 -1
  351. package/server/service/twin-backfill/backfill-period-facts.ts +168 -0
  352. package/server/service/twin-backfill/backfill-shape.ts +86 -0
  353. package/server/service/twin-backfill/index.ts +3 -0
  354. package/server/service/twin-backfill/twin-backfill-resolver.ts +35 -0
  355. package/server/service/twin-control/twin-control-mutation.ts +2 -2
  356. package/server/service/twin-event/twin-event-keys.ts +2 -2
  357. package/server/service/twin-event/twin-event-type.ts +18 -1
  358. package/server/service/twin-event/twin-event.ts +32 -6
  359. package/server/service/twin-forecast/forecast-metrics.ts +1 -1
  360. package/server/service/twin-forecast/gap-analytics.ts +1 -1
  361. package/server/service/twin-forecast/twin-forecast-query.ts +30 -11
  362. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +1 -1
  363. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +1 -1
  364. package/server/service/twin-instance/twin-instance.ts +4 -4
  365. package/server/service/twin-journal/event-type-split-shape.ts +112 -0
  366. package/server/service/twin-journal/event-type-split.ts +144 -0
  367. package/server/service/twin-journal/journal-order.ts +20 -0
  368. package/server/service/twin-journal/twin-journal-query.ts +224 -20
  369. package/server/service/twin-lifecycle/domain-catalog.ts +2 -1
  370. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +58 -4
  371. package/server/service/twin-metrics/twin-metrics-query.ts +14 -1
  372. package/server/service/twin-model/axis-journal-evidence.ts +1 -1
  373. package/server/service/twin-model/epcis-coverage.ts +34 -3
  374. package/server/service/twin-model/iec61850-coverage.ts +1 -1
  375. package/server/service/twin-model/isa95-coverage.ts +43 -16
  376. package/server/service/twin-model/item-ref.ts +2 -2
  377. package/server/service/twin-model/name-index.ts +2 -2
  378. package/server/service/twin-model/project-structure.ts +7 -6
  379. package/server/service/twin-model/twin-equipment.ts +16 -4
  380. package/server/service/twin-model/twin-lineage-query.ts +16 -5
  381. package/server/service/twin-model/twin-location.ts +1 -1
  382. package/server/service/twin-model/twin-model-item-query.ts +15 -15
  383. package/server/service/twin-model/twin-model-mutation.ts +4 -4
  384. package/server/service/twin-model/twin-model-query.ts +49 -28
  385. package/server/service/twin-model/twin-model-tree-query.ts +26 -2
  386. package/server/service/twin-model/twin-operation.ts +2 -2
  387. package/server/service/twin-space/move-space.ts +3 -3
  388. package/server/service/twin-space/representation-role.ts +15 -0
  389. package/server/service/twin-space/space-integrity.ts +1 -1
  390. package/server/service/twin-space/twin-space-representation.ts +37 -0
  391. package/server/service/twin-space/twin-space-resolver.ts +40 -8
  392. package/server/service/twin-structure/twin-structure.ts +3 -3
  393. package/server/service/twin-subject/event-subjects.ts +155 -0
  394. package/server/service/twin-subject/index.ts +10 -0
  395. package/server/service/twin-subject/item-resolver.ts +92 -0
  396. package/server/service/twin-subject/subject-query-shape.ts +84 -0
  397. package/server/service/twin-subject/twin-subject-backfill.ts +196 -0
  398. package/server/service/twin-subject/twin-subject-event.ts +101 -0
  399. package/server/service/twin-subject/twin-subject-fast-path.ts +346 -0
  400. package/server/service/twin-subject/twin-subject-query.ts +210 -0
  401. package/server/service/twin-subject/twin-subject-writer.ts +189 -0
  402. package/server/service/twin-target/twin-target-resolver.ts +1 -1
  403. package/shared/entity-delta.ts +4 -4
  404. package/shared/touched-items.ts +2 -2
  405. package/shared/twin-level.ts +1 -1
  406. package/test/adopt-structure-live.test.ts +3 -3
  407. package/test/aggregate-not-findone.test.ts +112 -0
  408. package/test/attention-digest.test.ts +6 -6
  409. package/test/axis-read.test.ts +6 -5
  410. package/test/backfill-shape.test.ts +89 -0
  411. package/test/boot-resume.test.ts +20 -7
  412. package/test/broadcast-cost-baseline.test.ts +2 -1
  413. package/test/broadcast-period.test.ts +2 -2
  414. package/test/broadcast-queue-overflow.test.ts +244 -0
  415. package/test/canonical-ingest-vocabularies.test.ts +259 -8
  416. package/test/canonical-quantity-door.test.ts +2 -2
  417. package/test/capability-mapping.test.ts +1 -1
  418. package/test/checkpoint-refuses-empty.test.ts +2 -2
  419. package/test/connector-capability-declaration.test.ts +88 -0
  420. package/test/contract-layer-guard.test.ts +48 -0
  421. package/test/control-capability.test.ts +1 -1
  422. package/test/cursor-stall-not-read-failure.test.ts +2 -2
  423. package/test/declaration-reaches-model.test.ts +7 -5
  424. package/test/declared-stimulus.test.ts +4 -4
  425. package/test/discovery-result.test.ts +2 -2
  426. package/test/entity-delta.test.ts +4 -4
  427. package/test/equipment-identity-reaches.test.ts +57 -0
  428. package/test/event-subjects.test.ts +147 -0
  429. package/test/event-time-column.test.ts +3 -3
  430. package/test/event-type-split.test.ts +155 -0
  431. package/test/fact-scope-wiring.test.ts +96 -0
  432. package/test/fold-gap-speaks.test.ts +53 -0
  433. package/test/forecast-metrics.test.ts +1 -1
  434. package/test/forecast-tuning.test.ts +1 -1
  435. package/test/generated-not-counted.test.ts +99 -0
  436. package/test/ingest-bench.test.ts +2 -1
  437. package/test/ingest-health-engine.test.ts +2 -2
  438. package/test/ingest-health-wiring.test.ts +7 -5
  439. package/test/ingest-health.test.ts +6 -6
  440. package/test/ingest-history.test.ts +2 -2
  441. package/test/ingest-idempotent.test.ts +132 -0
  442. package/test/ingest-space.test.ts +3 -3
  443. package/test/ingest-wiring-guard.test.ts +197 -0
  444. package/test/integration-probes.test.ts +3 -3
  445. package/test/integration-runner.test.ts +1 -1
  446. package/test/item-ref.test.ts +1 -1
  447. package/test/journal-order-axis.test.ts +80 -0
  448. package/test/journal-read-discipline.test.ts +16 -6
  449. package/test/journal-retention.test.ts +171 -29
  450. package/test/journal-sort-axis.test.ts +1 -1
  451. package/test/journal-write-door.test.ts +3 -3
  452. package/test/journal-write-trend.test.ts +1 -1
  453. package/test/kernel-kind-guard.test.ts +3 -3
  454. package/test/knob-defaults.test.ts +1 -1
  455. package/test/kpi-baseline-db.test.ts +2 -2
  456. package/test/kpi-fold.test.ts +266 -14
  457. package/test/kpi-query-bench.test.ts +3 -3
  458. package/test/live-cursor-wiring.test.ts +6 -6
  459. package/test/live-kernel-facts.test.ts +11 -11
  460. package/test/live-mirror-parity.test.ts +9 -6
  461. package/test/load-meter.test.ts +1 -1
  462. package/test/local-declarations.test.ts +13 -12
  463. package/test/log-stamp.test.ts +1 -1
  464. package/test/master-to-twin.test.ts +2 -2
  465. package/test/measured-yield.test.ts +2 -2
  466. package/test/merge-streams.test.ts +70 -0
  467. package/test/mirror-resumes-from-checkpoint.test.ts +13 -13
  468. package/test/model-basis.test.ts +2 -2
  469. package/test/model-gap.test.ts +4 -4
  470. package/test/model-vocabulary.test.ts +2 -2
  471. package/test/move-space.test.ts +1 -1
  472. package/test/oee-accumulator.test.ts +20 -5
  473. package/test/operational-vocabulary.test.ts +2 -2
  474. package/test/project-structure-db.test.ts +2 -2
  475. package/test/projection-reaches-screen.test.ts +7 -2
  476. package/test/projection-reads-declared.test.ts +109 -0
  477. package/test/property-effects.test.ts +10 -6
  478. package/test/read-failure-ledger.test.ts +106 -0
  479. package/test/read-failure-visible.test.ts +7 -7
  480. package/test/read-time.test.ts +118 -0
  481. package/test/reference-grounding.test.ts +1 -1
  482. package/test/reference-hook.test.ts +86 -0
  483. package/test/registry-key-guard.test.ts +1 -1
  484. package/test/rename-twin.test.ts +81 -0
  485. package/test/restart-policy.test.ts +3 -3
  486. package/test/resync-origin-site.test.ts +3 -3
  487. package/test/revision-axis.test.ts +14 -8
  488. package/test/runtime-key.test.ts +3 -3
  489. package/test/scale-twin-bench.test.ts +2 -1
  490. package/test/snapshot-freshness.test.ts +4 -4
  491. package/test/source-outcome-audit.test.ts +1 -1
  492. package/test/space-integrity.test.ts +1 -1
  493. package/test/space-representation.test.ts +66 -0
  494. package/test/space-scope-includes-stopped.test.ts +55 -0
  495. package/test/spec-coverage.test.ts +1 -1
  496. package/test/standard-coverage.test.ts +19 -21
  497. package/test/status-tally.test.ts +1 -1
  498. package/test/structure-revision-db.test.ts +7 -7
  499. package/test/subject-fast-path.test.ts +148 -0
  500. package/test/subject-read-model.test.ts +175 -0
  501. package/test/touched-items.test.ts +2 -2
  502. package/test/twin-audit.test.ts +1 -1
  503. package/test/twin-event-keys.test.ts +1 -1
  504. package/test/twin-model-item-db.test.ts +5 -5
  505. package/test/twin-model-tree-db.test.ts +35 -2
  506. package/test/twin-origin-resync.test.ts +1 -1
  507. package/test/vocabulary-guard.test.ts +1 -1
  508. package/test/warm-start-seam.test.ts +1 -1
  509. package/test/warm-start.test.ts +6 -6
  510. package/test/withheld-door.test.ts +2 -2
  511. package/test/yield-loop.test.ts +4 -4
  512. package/tsconfig.shared.tsbuildinfo +1 -1
  513. package/tsconfig.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ingest-health.js","sourceRoot":"","sources":["../../server/engine/ingest-health.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 동기화 건강 — **이 트윈이 현장과 맞춰지고 있나.** 순수(import 없음).\n *\n * ── 한 질문 ─────────────────────────────────────────────────────────────────\n * 화면에서 값이 변하지 않을 때 원인이 넷인데, 지금까지 **넷이 똑같이 보였다.**\n * ① 피드가 붙지 않았다 ② 붙었는데 아무것도 오지 않는다\n * ③ 오는데 전부 거부된다 ④ 정상인데 현장이 조용하다\n * ③은 특히 나쁘다 — 커넥터의 매핑이 한 글자 틀리면 트윈은 「빈 공장」이 되고, 그 사실이 서버 로그\n * 한 줄에만 남았다. 현장에 나간 사람은 로그를 뒤져야 했다.\n *\n * ── 왜 저널로는 답할 수 없나 ────────────────────────────────────────────────\n * **거부된 것은 저널에 없다** — 거부의 뜻이 그것이다. 저널을 접으면 통과한 쪽만 나온다. 거부의 추이는\n * 세는 자리를 따로 두어야 나온다. 이 장부가 그 자리다.\n *\n * ── 왜 건마다 저장하지 않나 ─────────────────────────────────────────────────\n * 라이브 계측은 초당 여러 건이다. 거부마다 행을 쓰면 그 자체가 부하가 된다(`reference-live.ts` 가\n * 이미 그렇게 적어 두었다). 그리고 이 패키지는 **`integration-base` 를 알지 않는다**(이식성 규약) —\n * 시나리오 경로가 쓰는 dead-letter 를 여기서 부를 수 없다. 그래서 **집계한다.**\n *\n * ── 통과와 거부를 함께 센다 ─────────────────────────────────────────────────\n * 거부만 그리면 「유입 없음」과 「정상」이 **둘 다 거부 0** 으로 같아 보인다. 제시 수가 있어야 갈린다.\n *\n * ── 지키는 규율 넷 ──────────────────────────────────────────────────────────\n * ① **닫힌 창만 「최근」이다** — 채우는 중인 창은 과소 집계다(부하 계기판에서 겪었다).\n * ② **0 과 「없음」을 가른다** — 안 받은 것은 `null`, 받았지만 거부 0 은 `0`.\n * ③ **조용히 자르지 않는다** — 표본·추이 상한에서 밀려난 수를 함께 낸다.\n * ④ **통과율에 1 을 함부로 쓰지 않는다** — 1,000건 중 1건 거부는 0.999 다.\n */\n\n/** 추이 창 하나 — 제시·통과·거부·미전달, 그리고 **적힌 것**을 함께 든다. */\nexport interface IngestWindow {\n /** 창이 열린 시각(ms). */\n startMs: number\n /** 창이 닫힌 시각(ms). 아직 채우는 중이면 `-1`. */\n endMs: number\n /** 이 창에 제시된 레코드 수(거부 여부 무관). */\n offered: number\n /** 그중 **매핑이 거부한** 수 — 처방은 매핑을 고치는 것이다. */\n rejected: number\n /**\n * 매핑은 통과했지만 **트윈에 닿지 못한** 수 — 처방은 트윈을 세우는 것이다.\n *\n * ── 왜 거부와 따로 세나 (2026-08-20에 겪었다) ─────────────────────────────\n * 트윈이 라이브로 돌지 않으면 커널이 레코드를 **조용히 버린다**. 그런데 장부는 그것을 「통과」로 셌다.\n * 화면이 「150 들어옴 · 150 통과 · 100%」라고 말하는 동안 150건은 바닥에 버려지고 있었다. 이 기능이\n * 막으려던 바로 그것을 이 기능이 저지른 셈이다.\n *\n * 거부에 합치는 것도 답이 아니다 — 거부는 「매핑을 고쳐라」는 말이고, 여기서 필요한 말은 「트윈이 안\n * 돌고 있다」다. **처방이 다르면 다른 수로 센다.**\n */\n undelivered: number\n /**\n * 이 창에 **저널에 적힌 행** 수 — 「무엇이 남았나」.\n *\n * 유입과 다른 축이다: 시뮬 트윈은 받는 것 없이 자기가 낸 사실을 적는다(그래서 `offered` 는 0 이고\n * `rows` 는 0 이 아니다). 이 칸이 없으면 그 트윈의 활동은 어느 추이에도 나타나지 않는다.\n */\n rows: number\n /**\n * 그것을 적기 위해 **DB 를 두드린 횟수**(flush) — 「우리가 몇 번 갔나」.\n *\n * 행 수와 따로 세는 이유: 2026-08-20 에 고친 것이 바로 이 값이다(행마다 한 번 → 창당 한 번). 행만\n * 세면 그 개선도, 되돌아가는 것도 보이지 않는다.\n */\n flushes: number\n}\n\n/** 거부 표본 하나 — 매핑을 고치려면 실제 모양을 봐야 한다. */\nexport interface RejectSample {\n atMs: number\n /** 커널이 낸 사유. 우리가 다시 쓰지 않고 그대로 옮긴다. */\n errors: string[]\n /** 막힌 레코드의 직렬화. 길면 자르고, 잘랐다고 표시한다. */\n record: string\n recordTruncated?: boolean\n}\n\nexport interface IngestLedger {\n /** 지금 채우는 중인 창. `startMs < 0` 이면 아직 아무것도 받지 않았다. */\n window: IngestWindow\n /** 닫힌 창들 — 오래된 것이 앞. 상한을 넘으면 앞에서 밀려난다. */\n trend: IngestWindow[]\n /** 추이에서 밀려난 창 수. */\n trendDropped: number\n /** 누적 제시·거부·미전달. */\n offered: number\n rejected: number\n undelivered: number\n /** 누적 — 적힌 행과 두드린 횟수. */\n rows: number\n flushes: number\n /**\n * 마지막으로 **적은** 시각. 한 번도 없으면 `-1`.\n *\n * 유입 시각(`lastAtMs`)과 갈라 두는 이유는 위 `recordJournalWrite` 주석에 있다 — 섞으면 끊긴 피드가\n * 초록으로 보인다.\n */\n lastWriteAtMs: number\n /**\n * 마지막으로 **버려진** 시각(ms). 한 번도 없으면 `-1`.\n *\n * 창과 따로 둔다: 미전달은 창이 닫히기 전에 알아야 한다(그 사이에도 사실은 계속 사라지고 있다).\n */\n lastUndeliveredAtMs: number\n /** 사유별 누적 건수와 **마지막으로 그 사유가 난 시각** — 고친 사유는 시각이 멈춘다. */\n byReason: Record<string, { count: number; lastAtMs: number }>\n /**\n * 사유 종류가 상한에 닿았나.\n *\n * **접힌 종류 수는 세지 않는다** — 그것을 세려면 이미 본 사유를 모두 기억해야 하고, 그 집합이\n * 바로 이 상한으로 막으려던 무한한 집합이다. 접힌 **건수**는 `byReason[OTHER_REASON].count` 가 답한다.\n */\n reasonsCapped: boolean\n /** 표본을 이미 뜬 사유들 — 같은 사유를 다시 직렬화하지 않기 위한 색인(O(1) 조회). */\n sampledReasons: Set<string>\n /** 최근 거부 표본. */\n samples: RejectSample[]\n /** 표본에서 밀려난 거부 건수. */\n sampleDropped: number\n /** 마지막으로 무엇이든 받은 시각(ms). **한 번도 없으면 `-1`** — 0 으로 두면 시각 0 과 구별되지 않는다. */\n lastAtMs: number\n /**\n * **원본에 닿지 못한 사실** (2026-08-23) — 「받은 것이 없다」와 **다른 사실**이다.\n *\n * ── 무엇이 조용했나 ─────────────────────────────────────────────────────────\n * 실 원본이 끊겼을 때(접속 시간 초과) 트윈의 **조회 가능한 상태 어디에도** 그 사실이 없었다.\n * `recordIngest` 는 `onRecords` 가 불릴 때만 도달하고, 읽기가 실패하면 그 문에 오지 않는다.\n * `TwinReference.lastError` 는 마스터 동기·접속 시험에만 적힌다.\n *\n * 그래서 화면이 볼 수 있는 것은 「새 사실이 없다」뿐이었고, 그것은 **「원본이 조용하다」와 구별되지\n * 않는다.** 실증 데모 중에 원본이 끊기면 사용자가 원인을 찾을 수 없다.\n *\n * ── 왜 「몇 번」과 「언제부터」를 함께 두나 ──────────────────────────────────\n * 한 번 실패한 것과 20분째 못 읽는 것은 다른 사실이다. 그 구별이 없으면 화면이 순간의 실패에 놀라거나\n * 계속된 단절을 대수롭지 않게 말한다. 그래서 **연속 실패 수**와 **처음 실패한 시각**을 함께 든다.\n *\n * 성공하면 지운다 — 고쳐진 단절이 화면에 남아 있으면 그것도 거짓이다.\n */\n readFailure?: {\n /** 연속 실패 수 — 성공 한 번에 0 으로 돌아간다. */\n consecutive: number\n /** 이 단절이 **처음** 시작된 시각(ms). 연속이 끊기면 다시 정해진다. */\n sinceMs: number\n /** 마지막 실패 시각(ms). */\n lastAtMs: number\n /** 마지막 실패의 사유 — 사람이 읽는 한 줄(원본이 준 말 그대로). */\n reason: string\n /** 어느 흐름에서 났나(어댑터가 말해 주면). 원본마다 흐름이 다르므로 열려 있다. */\n stream?: string\n }\n /**\n * **읽었는데 창을 넘길 수 없다** (2026-08-24) — 위와 **다른 사실**이다.\n *\n * ── 왜 갈랐나 ───────────────────────────────────────────────────────────────\n * 이 칸을 만들기 전에는 두 사실이 같은 이름으로 나갔다.\n *\n * readFailure 원본에 닿지 못했다 → **기다리면 풀린다**\n * cursorStall 읽었는데 커서가 못 넘어간다 → **기다려도 안 풀린다**\n *\n * 둘째는 읽기가 성공한 실패다: 한 시각에 한 페이지보다 많은 행이 몰려 커서를 그 시각 밖으로 옮길 수\n * 없다. 화면이 「원본에 닿지 못한다」고 말하면 사람은 원본을 의심하고 기다리는데, 필요한 조치는\n * 페이지를 키우거나 같은 시각 안에서 순서를 정하는 것이다. **조치가 반대인 두 사실을 한 이름으로\n * 부르면 그 이름은 정보가 아니라 오해다.**\n *\n * 모양은 위와 같게 둔다 — 「몇 번」과 「언제부터」가 여기서도 같은 값을 한다(한 번의 밀림과 사흘째\n * 못 넘어가는 것은 다르다). 넘어가면 지운다.\n */\n /**\n * **원본이 말했는데 트윈에 세우지 않은 것** — 이유별로 센다.\n *\n * ── 왜 이 자리가 필요한가 (2026-08-24) ─────────────────────────────────────\n * 커넥터가 원본의 사실 일부를 **일부러 받지 않는 일이 정상이다**: 입자가 맞지 않거나, 받으면 상태가\n * 거짓이 되거나, 커널 어휘로 옮길 수 없다. 그때 커넥터는 경고를 낸다 — **그런데 그 경고가 프로비저닝\n * 화면에서 한 번 스쳐 지나갈 뿐이었다.** 그 뒤로는 어디에서도 볼 수 없었다.\n *\n * 그래서 사용자가 「원본보다 계획이 적다」를 물으면 **답이 어디에도 없다.** 그 사실은 한 번의\n * 사건이 아니라 **상태의 성질**이다 — 세우지 않은 것은 지금도 트윈에 없다.\n *\n * ── `unhandled` 의 형제다 ──────────────────────────────────────────────────\n * `unhandled` 리듀서가 **받았는데** 담을 자리를 몰랐다\n * 여기 커넥터가 **보지만 일부러 보내지 않았다** — 이유와 함께\n *\n * 둘 다 「사실이 조용히 사라지지 않게」 세는 것이고, 이유가 다르므로 자리도 다르다.\n *\n * 이유를 **문자열로 받는다**: 무엇을 왜 안 받는지는 원본마다 다르고, 계약이 그 목록을 닫으면 새 원본을\n * 붙일 때마다 계약을 고치게 된다(흐름 열쇠와 같은 규율).\n */\n withheld?: { reason: string; count: number; lastAtMs: number }[]\n cursorStall?: {\n /** 연속 정체 수 — 넘어가면 0 으로 돌아간다. */\n consecutive: number\n /** 이 정체가 **처음** 시작된 시각(ms). */\n sinceMs: number\n /** 마지막 정체 시각(ms). */\n lastAtMs: number\n /** 사유 — 사람이 읽는 한 줄(어느 시각에서 막혔나까지 어댑터가 말해 주면 좋다). */\n reason: string\n /** 어느 흐름인가. 밀도가 높은 표는 원본마다 다르므로 이 이름이 조치의 절반이다. */\n stream?: string\n }\n}\n\n/**\n * 읽기 실패를 장부에 적는다 — **받은 것이 없는 것과 가른다.**\n *\n * 연속 실패의 시작 시각을 지키는 것이 이 함수의 요점이다: 매번 새로 적으면 「20분째 못 읽는다」를\n * 말할 수 없고, 화면은 늘 「방금 실패했다」만 보게 된다.\n */\nexport function recordReadFailure(\n ledger: IngestLedger,\n reason: string,\n nowMs: number,\n stream?: string\n): void {\n const prev = ledger.readFailure\n ledger.readFailure = {\n consecutive: (prev?.consecutive ?? 0) + 1,\n sinceMs: prev?.sinceMs ?? nowMs,\n lastAtMs: nowMs,\n reason,\n ...(stream ? { stream } : {})\n }\n}\n\n/**\n * 읽기가 성공했다 — 단절 기록을 지운다.\n *\n * **빈 읽기도 성공이다**: 원본이 「새 것이 없다」고 답한 것은 닿았다는 뜻이다. 그 둘을 같게 두면\n * 조용한 원본이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).\n */\nexport function clearReadFailure(ledger: IngestLedger): void {\n if (ledger.readFailure) delete ledger.readFailure\n}\n\n/**\n * 커서 정체를 장부에 적는다 — **닿지 못한 것과 가른다.**\n *\n * `recordReadFailure` 와 같은 모양이지만 **다른 칸**이다. 한 주기가 두 이유로 실패할 수는 없으므로\n * (먼저 닿아야 읽는다) 둘이 동시에 서지 않는다 — 그래서 화면은 둘 중 하나만 보게 되고, 그 하나가\n * 조치를 정한다.\n */\nexport function recordCursorStall(ledger: IngestLedger, reason: string, nowMs: number, stream?: string): void {\n const prev = ledger.cursorStall\n ledger.cursorStall = {\n consecutive: (prev?.consecutive ?? 0) + 1,\n sinceMs: prev?.sinceMs ?? nowMs,\n lastAtMs: nowMs,\n reason,\n ...(stream ? { stream } : {})\n }\n}\n\n/**\n * **세우지 않은 것을 적는다** — 이유별 누적.\n *\n * 주기마다 다시 알려도 시끄러워지지 않게 **이유로 묶어 센다**(줄이 늘지 않는다). 그리고 마지막 시각을\n * 함께 든다 — 「지금도 그런가」와 「한때 그랬나」는 다른 사실이고, 시각 없이는 구별할 수 없다.\n *\n * 이유가 같고 수가 달라지면 **마지막 수로 덮는다**: 이것은 누적 사건이 아니라 **지금 세우지 않은 것의\n * 수**다. 주기마다 8건이면 8이고, 더해서 800이 되면 거짓이다.\n */\nexport function recordWithheld(ledger: IngestLedger, reason: string, count: number, nowMs: number): void {\n if (!reason || !(count > 0)) return\n const list = (ledger.withheld ??= [])\n const at = list.find(w => w.reason === reason)\n if (at) {\n at.count = count\n at.lastAtMs = nowMs\n return\n }\n list.push({ reason, count, lastAtMs: nowMs })\n}\n\n/** 창을 넘겼으면 지운다 — 풀린 정체가 화면에 남아 있으면 그것도 거짓이다. */\nexport function clearCursorStall(ledger: IngestLedger): void {\n if (ledger.cursorStall) delete ledger.cursorStall\n}\n\n/** 창 길이 — 10분. 부하 계기판(10초)과 다른 값이라 이름을 따로 둔다(같은 이름이면 섞인다). */\nexport const INGEST_WINDOW_MS = 600_000\n\n/**\n * 이만큼 아무것도 오지 않으면 「유입 없음」이라 말한다.\n *\n * 창 하나와 같게 둔다 — 한 창을 통째로 비운 것은 실제로 「그 구간에 아무것도 오지 않았다」이고, 추이의\n * 그 자리도 비게 된다. 그림과 알약이 같은 말을 하게 하려면 같은 길이여야 한다.\n */\nexport const STALE_AFTER_MS = INGEST_WINDOW_MS\n\n/** 추이 보관 창 수 — 36창 × 10분 = 6시간. 「고친 것이 먹혔나」를 보기에 충분하고 메모리는 숫자 넷×36 이다. */\nexport const TREND_LIMIT = 36\n\n/**\n * 사유 종류 상한.\n *\n * **없으면 메모리가 끝없이 자란다.** 사유 문구에 레코드마다 다른 값이 섞이는 커넥터가 하나만 있어도\n * (`unknown bizStep \"PUTAWAY_X17\"`, `…X18`, …) 키가 무한히 늘어난다. 초당 1만 건 규모에서 이것은\n * 미미한 오버헤드가 아니라 누수다.\n *\n * 넘치면 `OTHER_REASON` 한 칸에 모으고 **몇 종을 접었는지 함께 낸다**(조용히 자르지 않는다).\n * 30종이면 고칠 곳을 찾기에 충분하다 — 목록을 훑는 일이 아니라 매핑 몇 줄을 고치는 일이다.\n */\nexport const REASON_LIMIT = 30\n\n/** 상한을 넘긴 사유들이 모이는 칸. */\nexport const OTHER_REASON = '(other reasons)'\n\n/**\n * 표본 상한 — **사유마다 한 건**만 든다.\n *\n * 같은 사유 20건보다 **서로 다른 사유 1건씩**이 매핑을 고치는 데 유용하다. 그리고 이 규칙이 성능을\n * 지킨다: 그 사유의 표본이 이미 있으면 `JSON.stringify` 를 **아예 하지 않는다.** 실측(레코드 241바이트)\n * 으로 건당 0.24µs 인데, 초당 1만 건이 전부 거부되면 9,980건은 만들자마자 버려지는 문자열이었다 —\n * CPU 는 감당되지만 GC 압박이 이벤트 루프로 돌아온다.\n */\nexport const SAMPLE_LIMIT = REASON_LIMIT\n\n/** 표본 하나의 직렬화 상한(문자). */\nexport const RECORD_MAX = 2000\n\n/**\n * 매핑이 막고 있다고 볼 통과율 문턱.\n *\n * 값과 함께 내보낸다 — 화면이 「95% 미만이라 매핑 문제로 봤다」를 말할 수 있게. 숨긴 문턱은 사람이\n * 판정을 믿지 못한다.\n */\nexport const HEALTHY_ACCEPTED_RATIO = 0.95\n\nexport function newIngestLedger(): IngestLedger {\n return {\n window: { startMs: -1, endMs: -1, offered: 0, rejected: 0, undelivered: 0, rows: 0, flushes: 0 },\n trend: [],\n trendDropped: 0,\n offered: 0,\n rejected: 0,\n undelivered: 0,\n rows: 0,\n flushes: 0,\n lastWriteAtMs: -1,\n lastUndeliveredAtMs: -1,\n byReason: {},\n reasonsCapped: false,\n sampledReasons: new Set<string>(),\n samples: [],\n sampleDropped: 0,\n lastAtMs: -1\n }\n}\n\n/**\n * 창이 다 찼으면 닫아 추이에 넣고 새로 연다. **닫힌 창만 「최근」으로 읽는다.**\n *\n * ── 영속이 붙는 유일한 자리 ────────────────────────────────────────────────\n * 닫힌 창 하나가 영속의 단위다(숫자 넷 + 구간). `onClosed` 를 받는 이유는 **이 함수가 창이 닫히는\n * 유일한 지점**이기 때문이다 — 여기 한 곳에 붙이면 기록 경로가 갈리지 않는다.\n *\n * 왜 건마다가 아니라 창마다인가: 거부마다 행을 쓰면 초당 1만 행이고 그 부하가 이벤트 루프로 돌아온다.\n * 창마다면 트윈 1,000개에 하루 14.4만 행(약 14MB)이다.\n *\n * `onClosed` 는 **오류를 내지 않아야 한다**(호출부가 감싼다) — 영속이 실패해도 장부는 계속 굴러야 한다.\n * 영속을 지키는 것과 관측을 지키는 것은 다른 일이고, 뒤엣것이 앞엣것 때문에 멈추면 안 된다.\n */\nexport function rollIngestWindow(\n ledger: IngestLedger,\n nowMs: number,\n windowMs = INGEST_WINDOW_MS,\n onClosed?: (closed: IngestWindow) => void\n): void {\n if (ledger.window.startMs < 0) {\n ledger.window.startMs = nowMs\n return\n }\n if (nowMs - ledger.window.startMs < windowMs) return\n\n const closed: IngestWindow = { ...ledger.window, endMs: nowMs }\n ledger.trend.push(closed)\n ledger.window = { startMs: nowMs, endMs: -1, offered: 0, rejected: 0, undelivered: 0, rows: 0, flushes: 0 }\n\n if (ledger.trend.length > TREND_LIMIT) {\n const drop = ledger.trend.length - TREND_LIMIT\n ledger.trend.splice(0, drop)\n ledger.trendDropped += drop\n }\n\n /* 영속은 관측을 막지 않는다 — 터져도 장부는 이미 갱신됐다. */\n if (onClosed) {\n try {\n onClosed(closed)\n } catch {\n /* 호출부가 알린다. 여기서 삼키는 것은 **장부의 진행**을 지키기 위해서다. */\n }\n }\n}\n\n/** 사유 묶음의 키. 사유가 여럿이면 한 줄로 잇고, 없으면 지어내지 않고 `unknown`. */\nexport function reasonKey(errors: string[] | undefined): string {\n const list = (errors ?? []).map(e => String(e).trim()).filter(Boolean)\n return list.length ? list.join(' · ') : 'unknown'\n}\n\nfunction safeJson(v: unknown): string {\n try {\n const s = JSON.stringify(v)\n return s === undefined ? String(v) : s\n } catch {\n return '(serialization failed)' /* 순환 참조 등 — 모양을 못 보여 주는 것도 사실이다 */\n }\n}\n\n/**\n * 한 번의 인제스트 결과를 적는다.\n *\n * `offered` 를 함께 받는 이유: 통과율을 **서로 다른 두 계수기**에서 나눠 계산하면 분모와 분자가 다른\n * 것을 세게 된다. 둘을 한자리에서 본 수를 그대로 적는다.\n */\nexport function recordIngest(\n ledger: IngestLedger,\n offered: number,\n rejected: { record?: unknown; errors?: string[] }[] | undefined,\n nowMs: number,\n onClosed?: (closed: IngestWindow) => void,\n /** 매핑은 통과했지만 트윈에 닿지 못한 수 — 「통과」로 세면 화면이 거짓을 말한다. */\n undelivered = 0\n): void {\n if (!Number.isFinite(offered) || offered < 0) return\n\n rollIngestWindow(ledger, nowMs, INGEST_WINDOW_MS, onClosed)\n\n ledger.offered += offered\n ledger.window.offered += offered\n ledger.lastAtMs = nowMs\n\n if (Number.isFinite(undelivered) && undelivered > 0) {\n ledger.undelivered += undelivered\n ledger.window.undelivered += undelivered\n ledger.lastUndeliveredAtMs = nowMs\n }\n\n const items = rejected ?? []\n if (!items.length) return\n\n ledger.rejected += items.length\n ledger.window.rejected += items.length\n\n for (const it of items) {\n const raw = reasonKey(it?.errors)\n const known = raw in ledger.byReason\n\n /* 사유 종류 상한 — 넘치면 한 칸으로 접고, **닿았다는 사실**을 표시한다. */\n let key = raw\n if (!known && Object.keys(ledger.byReason).length >= REASON_LIMIT) {\n key = OTHER_REASON\n ledger.reasonsCapped = true\n }\n\n const seen = ledger.byReason[key]\n ledger.byReason[key] = { count: (seen?.count ?? 0) + 1, lastAtMs: nowMs }\n\n /*\n * **표본은 사유마다 하나.** 이미 그 사유의 표본이 있으면 직렬화를 아예 하지 않는다 — 초당 1만 건이\n * 전부 거부될 때 만들자마자 버려지는 문자열을 만들지 않기 위해서다(실측: 건당 0.24µs, GC 압박).\n */\n if (ledger.sampledReasons.has(key)) {\n ledger.sampleDropped += 1\n continue\n }\n\n const text = safeJson(it?.record)\n const truncated = text.length > RECORD_MAX\n ledger.samples.push({\n atMs: nowMs,\n errors: key === OTHER_REASON ? [OTHER_REASON] : it?.errors?.length ? [...it.errors] : ['unknown'],\n record: truncated ? text.slice(0, RECORD_MAX) : text,\n ...(truncated ? { recordTruncated: true } : {})\n })\n\n ledger.sampledReasons.add(key)\n\n if (ledger.samples.length > SAMPLE_LIMIT) {\n const gone = ledger.samples.splice(0, ledger.samples.length - SAMPLE_LIMIT)\n /* 밀려난 표본의 사유는 색인에서도 빼야 다음에 다시 뜰 수 있다(안 빼면 그 사유는 영원히 표본 없음). */\n for (const g of gone) ledger.sampledReasons.delete(reasonKey(g.errors))\n ledger.sampleDropped += gone.length\n }\n }\n}\n\n/**\n * 저널에 **적은 것**을 기록한다 — 창을 넘길 때가 되었으면 넘기고, 행·왕복을 더한다.\n *\n * ── 왜 유입과 같은 창인가 ───────────────────────────────────────────────────\n * 새 장부를 만들지 않는다. 이 창은 「닫힌 창만 최근」·「0 과 없음을 가른다」·「잘린 것을 세어 드러낸다」를\n * 이미 지키고, 닫힐 때 영속까지 된다. 같은 규율을 두 벌 만들면 한쪽만 고쳐진다.\n *\n * ── 유입이 없어도 창은 넘어간다 ────────────────────────────────────────────\n * 시뮬 트윈은 받는 것 없이 자기가 낸 사실을 적는다. 창이 **유입에서만** 넘어간다면 그 트윈은 추이에\n * 영원히 나타나지 않는다 — 그래서 쓰기도 창을 넘긴다.\n */\nexport function recordJournalWrite(\n ledger: IngestLedger,\n rows: number,\n nowMs: number,\n onClosed?: (closed: IngestWindow) => void\n): void {\n if (!Number.isFinite(rows) || rows <= 0) return\n rollIngestWindow(ledger, nowMs, INGEST_WINDOW_MS, onClosed)\n ledger.rows += rows\n ledger.flushes += 1\n ledger.window.rows += rows\n ledger.window.flushes += 1\n /*\n * **`lastAtMs` 는 건드리지 않는다.** 그 값은 「마지막 유입」이고, 끊긴 피드를 가려내는 근거다. 쓰기로\n * 갱신하면 라이브 트윈은 유입이 끊겨도 틱이 만든 델타를 계속 적으므로 **영원히 「방금 받았다」**가 되고,\n * 「오래 끊긴 피드에 옛 창으로 초록을 내지 않는다」가 조용히 무력해진다. 축을 따로 든다.\n */\n ledger.lastWriteAtMs = nowMs\n}\n\n/**\n * 한눈 판정.\n *\n * `not-applicable` 과 `not-connected` 를 섞지 않고, 재지 않은 것을 정상·비정상으로 채우지 않는다\n * (`unknown`). 시뮬 트윈에 「동기화 안 됨」이라 말하면 거짓 경보이고, **거짓 경보는 참 경보를 죽인다.**\n */\nexport type SyncVerdict =\n | 'syncing' /** 유입이 있고 통과율이 문턱 이상 */\n | 'mapping-blocked' /** 유입은 있는데 거부가 문턱을 넘는다 — 사유를 보고 매핑을 고친다 */\n /**\n * 들어오는데 **커널이 받지 않는다** — 사실이 사라지고 있다.\n *\n * ── 이름이 사실보다 좁다 (2026-08-20에 드러났다) ────────────────────────────\n * 관측한 것은 「커널이 받지 않았다」(`ingestLive` 가 0)이고, **멈춘 것은 그 원인 중 하나일 뿐**이다.\n * 다른 원인이 실제로 있었다: 미러로 선언된 트윈을 사람이 화면에서 시작하면 시뮬 구동으로 떴고, 그\n * 트윈은 **돌고 있으면서** 유입을 한 건도 받지 못했다. 그때 화면은 한쪽에 「running」, 다른 쪽에\n * 「멈춰」라고 적었고 사람은 「시작 버튼을 누르면 되겠네」라는 엉뚱한 처방으로 갔다.\n *\n * 그래서 **화면 문구는 관측 그대로** 적는다(「트윈에 닿지 않고 버려지는 중」). 이 키 이름은 서버·화면·\n * 시험에 걸친 계약 문자열이라 그대로 두지만, 이름을 원인으로 읽지 않는다.\n */\n | 'twin-not-running'\n | 'no-traffic' /** 피드는 붙었는데 제시가 0 — 원천·망을 본다 */\n | 'not-connected' /** 피드 자체가 없다 — 결선한다 */\n | 'not-applicable' /** 원본이 먹이지 않는 트윈(시뮬)·멈춘 트윈 — 물을 대상이 아니다 */\n | 'unknown' /** 한 번도 받지 않았거나 창이 아직 닫히지 않았다 — 기다린다 */\n\nexport function syncVerdictOf(args: {\n /** `live-feed-registry.liveFeedStateOf` 의 답을 그대로 받는다 — 판정 어휘를 두 벌 만들지 않는다. */\n feedState: 'attached' | 'detached' | 'not-applicable'\n /** 마지막으로 **닫힌** 창. 없으면 `null`. */\n recent: { offered: number; rejected: number } | null\n /** 마지막으로 무엇이든 받은 시각. 한 번도 없으면 `-1` — 0 이 아니다(0 은 1970년이다). */\n lastAtMs?: number\n /** 지금. `lastAtMs` 를 견줄 때만 쓴다. */\n nowMs?: number\n /** 마지막으로 **버려진** 시각. 한 번도 없으면 `-1`. */\n lastUndeliveredAtMs?: number\n threshold?: number\n}): SyncVerdict {\n /*\n * ── 버려지고 있는 것이 가장 먼저다 ────────────────────────────────────────\n * 트윈이 멈추면 피드는 남아 계속 레코드를 나르고, 커널은 그것을 조용히 버린다. 그 상태를 「해당 없음」\n * 이라 부르면 **사실이 사라지는 동안 화면이 안심시킨다** — 실제로 그렇게 떴다(멈춘 트윈 옆에 「150\n * 통과 · 100%」). 그래서 시뮬·멈춤 판정보다 먼저 본다.\n *\n * 시뮬 트윈에는 이 값이 서지 않는다(먹이는 피드가 없으므로 미전달도 없다). 그래서 이 순서가 시뮬에\n * 거짓 경보를 주지 않는다.\n */\n if (\n typeof args.lastUndeliveredAtMs === 'number' &&\n args.lastUndeliveredAtMs >= 0 &&\n typeof args.nowMs === 'number' &&\n args.nowMs - args.lastUndeliveredAtMs <= STALE_AFTER_MS\n ) {\n return 'twin-not-running'\n }\n\n if (args.feedState === 'not-applicable') return 'not-applicable'\n if (args.feedState === 'detached') return 'not-connected'\n\n /*\n * ── 끊긴 피드가 초록으로 남지 않게 ────────────────────────────────────────\n * 창은 **다음 유입이 있을 때** 닫힌다. 그래서 유입이 멈추면 창도 멈추고, 「최근 창」은 멈추기 직전의\n * 건강했던 창으로 남는다. 그 창만 보고 판정하면 **어제 죽은 피드가 오늘도 「동기화 중」**이다. 이\n * 기능이 있는 이유가 바로 그 상태를 드러내는 것이므로, 그때 초록을 내면 목적을 놓친다.\n *\n * 그래서 창과 별개로 **지금까지의 침묵**을 먼저 본다. 한 창을 통째로 아무것도 받지 않았으면 그것은\n * 「유입 없음」이다 — 실제로 그 구간의 창은 비어 있게 된다.\n *\n * 이 판단을 서버에 두는 이유: 소비처가 여럿(목록·연결 상세·앞으로 생길 것)이고, 화면마다 다시\n * 판단하면 규칙이 갈라져 어떤 화면은 초록, 어떤 화면은 경고를 말한다.\n *\n * 한계 — 한 창(10분)보다 **느리게 긷는 원천**은 긷는 사이에 「유입 없음」으로 보인다. 장부는 원천의\n * 주기를 모르므로 이보다 정확히 답할 수 없다. 주기를 알게 되면 그 값으로 견주는 것이 맞다.\n */\n if (typeof args.lastAtMs === 'number' && typeof args.nowMs === 'number') {\n if (args.lastAtMs < 0) return 'unknown' /* 붙었지만 한 번도 받지 않았다 — 기다린다 */\n if (args.nowMs - args.lastAtMs > STALE_AFTER_MS) return 'no-traffic'\n }\n\n if (!args.recent) return 'unknown'\n if (args.recent.offered <= 0) return 'no-traffic'\n\n const ratio = (args.recent.offered - args.recent.rejected) / args.recent.offered\n return ratio >= (args.threshold ?? HEALTHY_ACCEPTED_RATIO) ? 'syncing' : 'mapping-blocked'\n}\n\nexport interface IngestWindowView {\n from: string\n to: string\n offered: number\n /** **트윈에 닿은** 수. 제시 − 거부 − 미전달. */\n accepted: number\n /** 매핑이 거부한 수 — 처방은 매핑을 고치는 것. */\n rejected: number\n /** 매핑은 통과했으나 트윈에 닿지 못한 수 — 처방은 트윈을 세우는 것. */\n undelivered: number\n /** 통과율(0~1). 제시가 없으면 `null` — **1 로 채우면 「전부 통과」로 읽힌다.** */\n acceptedRatio: number | null\n /** 이 창에 적힌 행. */\n rows: number\n /** 그것을 적기 위해 DB 를 두드린 횟수 — 행당 왕복이 줄었는지 보는 값. */\n flushes: number\n}\n\nexport interface IngestHealthView {\n verdict: SyncVerdict\n /** 판정에 쓴 문턱 — 화면이 왜 그 판정인지 말할 수 있게 함께 낸다. */\n threshold: number\n /** 마지막으로 닫힌 창. 창이 한 번도 닫히지 않았으면 `null`. */\n recent: IngestWindowView | null\n /** 닫힌 창들, 오래된 것부터. */\n trend: IngestWindowView[]\n /** 추이 상한에서 밀려난 창 수. */\n trendDropped: number\n /** 누적(기동 이후). */\n total: {\n offered: number\n accepted: number\n rejected: number\n undelivered: number\n acceptedRatio: number | null\n /** 저널에 적힌 행과 그것을 위해 DB 를 두드린 횟수 — 기동 이후 누적. */\n rows: number\n flushes: number\n }\n /** 사유별 누적, 많은 것부터. `lastAt` 이 멈춘 사유는 이미 고쳐진 것이다. */\n reasons: { reason: string; count: number; lastAt: string }[]\n /** 사유 종류가 상한에 닿아 일부가 `(other reasons)` 로 접혔나. */\n reasonsCapped: boolean\n samples: RejectSample[]\n sampleDropped: number\n /** 마지막으로 무엇이든 받은 시각. 한 번도 없으면 `null`. */\n lastAt: string | null\n /** 마지막으로 저널에 적은 시각. 한 번도 없으면 `null` — 받은 적 없는 트윈도 이 값은 있을 수 있다. */\n lastWriteAt: string | null\n /**\n * **원본에 닿지 못하고 있다** — 없으면 닿고 있다는 뜻이다(§`IngestLedger.readFailure`).\n *\n * 커서 정체(`cursorStall`)와 **섞이지 않는다**: 이쪽은 기다리면 풀리고 그쪽은 기다려도 안 풀린다.\n *\n * 「유입 없음」과 **다른 판정**이다: 유입 없음은 원본이 조용한 것일 수 있고, 이것은 우리가 묻지도\n * 못한 것이다. 화면은 두 문장을 다르게 말해야 한다 — 실 원본이 끊겼을 때 사용자가 원인을 찾을 수\n * 있는지가 그 차이다.\n */\n readFailure?: {\n consecutive: number\n /** 이 단절이 시작된 시각 — 「20분째」를 말할 수 있게. */\n since: string\n lastAt: string\n reason: string\n stream?: string\n }\n}\n\nfunction toWindowView(w: IngestWindow, fallbackEndMs: number): IngestWindowView {\n /* 「통과」는 **트윈에 닿은** 것만이다. 버려진 것을 통과로 세면 화면이 안심시킨다. */\n const accepted = w.offered - w.rejected - w.undelivered\n return {\n from: new Date(w.startMs).toISOString(),\n to: new Date(w.endMs >= 0 ? w.endMs : fallbackEndMs).toISOString(),\n offered: w.offered,\n accepted,\n rejected: w.rejected,\n undelivered: w.undelivered,\n acceptedRatio: w.offered > 0 ? round4(accepted / w.offered) : null,\n rows: w.rows,\n flushes: w.flushes\n }\n}\n\n/**\n * 읽기용 요약.\n *\n * 장부가 없어도(한 번도 받지 않은 트윈) **판정은 낸다** — 피드 사실만으로 답할 수 있는 경우가 있다\n * (`not-applicable`·`not-connected`). 그 경우 수치는 비운다: 없는 값을 0 으로 채우지 않는다.\n *\n * `nowMs` 를 받는 이유: 끊긴 피드를 가려내려면 「마지막 유입이 얼마나 전인가」를 봐야 하고, 그 판단을\n * 화면이 아니라 여기서 한다(소비처마다 다시 판단하면 규칙이 갈라진다). 시험이 시각을 고정할 수 있도록\n * 인자로 받는다.\n */\nexport function ingestHealth(\n ledger: IngestLedger | undefined,\n feedState: 'attached' | 'detached' | 'not-applicable',\n threshold = HEALTHY_ACCEPTED_RATIO,\n nowMs: number = Date.now()\n): IngestHealthView {\n const recentRaw = ledger?.trend.length ? ledger.trend[ledger.trend.length - 1] : null\n /* 열린 창의 끝 시각 대체값 — 유입이 없는 장부에서 `-1` 을 시각으로 읽으면 1969년이 화면에 뜬다. */\n const lastAnyMs = Math.max(ledger?.lastAtMs ?? -1, ledger?.lastWriteAtMs ?? -1)\n const verdict = syncVerdictOf({\n feedState,\n recent: recentRaw,\n lastAtMs: ledger?.lastAtMs ?? -1,\n lastUndeliveredAtMs: ledger?.lastUndeliveredAtMs ?? -1,\n nowMs,\n threshold\n })\n\n /*\n * **유입이 없어도 「빈 장부」가 아니다.** 시뮬 트윈은 받는 것 없이 적는다. 유입 시각만 보고 비었다고\n * 판단하면 그 트윈의 창은 만들어져 있는데 화면에는 아무 추이도 나오지 않는다(이 축을 넣은 이유가 그것이다).\n */\n if (!ledger || (ledger.lastAtMs < 0 && ledger.lastWriteAtMs < 0)) {\n return {\n verdict,\n threshold,\n recent: null,\n trend: [],\n trendDropped: 0,\n total: { offered: 0, accepted: 0, rejected: 0, undelivered: 0, acceptedRatio: null, rows: 0, flushes: 0 },\n reasons: [],\n reasonsCapped: false,\n samples: [],\n sampleDropped: 0,\n lastAt: null,\n lastWriteAt: null,\n /*\n * **이 짧은 답에도 단절은 실어야 한다** — 이 자리가 바로 그 트윈의 모습이다.\n *\n * 받은 것도 적은 것도 없는 장부는 「아직 아무 일도 없다」로 읽히는데, 원본에 닿지 못하는 트윈이\n * 정확히 그 모습이다(읽지 못하면 받은 것도 적은 것도 없다). 여기서 떨어뜨리면 **가장 필요한\n * 순간에** 그 사실이 사라진다 — 실측으로 원본이 끊긴 트윈이 이 경로로 답하고 있었다.\n */\n ...(ledger?.readFailure\n ? {\n readFailure: {\n consecutive: ledger.readFailure.consecutive,\n since: new Date(ledger.readFailure.sinceMs).toISOString(),\n lastAt: new Date(ledger.readFailure.lastAtMs).toISOString(),\n reason: ledger.readFailure.reason,\n ...(ledger.readFailure.stream ? { stream: ledger.readFailure.stream } : {})\n }\n }\n : {}),\n /* 커서 정체도 **이 갈래에서** 내보낸다 — 유입이 없는 장부에서도 정체는 실재한다(읽었으나 못\n 넘겼으면 유입이 0이다). 여기서 떨어뜨리면 바로 그 상황에서 조용해진다. */\n ...(ledger?.cursorStall\n ? {\n cursorStall: {\n consecutive: ledger.cursorStall.consecutive,\n since: new Date(ledger.cursorStall.sinceMs).toISOString(),\n lastAt: new Date(ledger.cursorStall.lastAtMs).toISOString(),\n reason: ledger.cursorStall.reason,\n ...(ledger.cursorStall.stream ? { stream: ledger.cursorStall.stream } : {})\n }\n }\n : {}),\n /*\n * **세우지 않은 것도 낸다 — 양쪽 갈래에서.** 유입이 한 건도 없는 장부에서도 이 사실은 실재한다\n * (원본이 말하는데 전부 안 받는 경우가 있다). 여기서 떨어뜨리면 바로 그 상황에서 조용해진다.\n */\n ...(ledger?.withheld?.length\n ? {\n withheld: ledger.withheld.map(w => ({\n reason: w.reason,\n count: w.count,\n lastAt: new Date(w.lastAtMs).toISOString()\n }))\n }\n : {})\n }\n }\n\n /* 「통과」는 트윈에 닿은 것만이다 — 창 뷰와 같은 규칙. */\n const accepted = ledger.offered - ledger.rejected - ledger.undelivered\n return {\n verdict,\n threshold,\n recent: recentRaw ? toWindowView(recentRaw, lastAnyMs) : null,\n trend: ledger.trend.map(w => toWindowView(w, lastAnyMs)),\n trendDropped: ledger.trendDropped,\n /* 세우지 않은 것 — 양쪽 갈래에서 같은 모양으로 낸다(§`IngestLedger.withheld`). */\n ...(ledger.withheld?.length\n ? {\n withheld: ledger.withheld.map(w => ({\n reason: w.reason,\n count: w.count,\n lastAt: new Date(w.lastAtMs).toISOString()\n }))\n }\n : {}),\n total: {\n offered: ledger.offered,\n accepted,\n rejected: ledger.rejected,\n undelivered: ledger.undelivered,\n acceptedRatio: ledger.offered > 0 ? round4(accepted / ledger.offered) : null,\n rows: ledger.rows,\n flushes: ledger.flushes\n },\n reasons: Object.entries(ledger.byReason)\n .map(([reason, v]) => ({ reason, count: v.count, lastAt: new Date(v.lastAtMs).toISOString() }))\n .sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason)),\n reasonsCapped: ledger.reasonsCapped,\n samples: [...ledger.samples],\n sampleDropped: ledger.sampleDropped,\n /* 「받은 적 없음」과 「방금 받음」을 가른다 — 적기만 하는 트윈의 유입 시각은 **없다**. */\n lastAt: ledger.lastAtMs >= 0 ? new Date(ledger.lastAtMs).toISOString() : null,\n lastWriteAt: ledger.lastWriteAtMs >= 0 ? new Date(ledger.lastWriteAtMs).toISOString() : null,\n /* 닿지 못하고 있으면 그 사실을 낸다 — 없으면 필드를 만들지 않는다(빈 값이 「닿는다」로 읽히지 않게). */\n ...(ledger.readFailure\n ? {\n readFailure: {\n consecutive: ledger.readFailure.consecutive,\n since: new Date(ledger.readFailure.sinceMs).toISOString(),\n lastAt: new Date(ledger.readFailure.lastAtMs).toISOString(),\n reason: ledger.readFailure.reason,\n ...(ledger.readFailure.stream ? { stream: ledger.readFailure.stream } : {})\n }\n }\n : {}),\n ...(ledger.cursorStall\n ? {\n cursorStall: {\n consecutive: ledger.cursorStall.consecutive,\n since: new Date(ledger.cursorStall.sinceMs).toISOString(),\n lastAt: new Date(ledger.cursorStall.lastAtMs).toISOString(),\n reason: ledger.cursorStall.reason,\n ...(ledger.cursorStall.stream ? { stream: ledger.cursorStall.stream } : {})\n }\n }\n : {})\n }\n}\n\n/** 비율은 자릿수를 넉넉히 준다 — 0.999 가 1 로 반올림되면 「전부 통과」로 읽힌다. */\nconst round4 = (v: number) => Math.round(v * 10000) / 10000\n"]}
1
+ {"version":3,"file":"ingest-health.js","sourceRoot":"","sources":["../../server/engine/ingest-health.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 동기화 건강 — **이 트윈이 현장과 맞춰지고 있나.** 순수(import 없음).\n *\n * ── 한 질문 ─────────────────────────────────────────────────────────────────\n * 화면에서 값이 변하지 않을 때 원인이 넷인데, 지금까지 **넷이 똑같이 보였다.**\n * ① 피드가 붙지 않았다 ② 붙었는데 아무것도 오지 않는다\n * ③ 오는데 전부 거부된다 ④ 정상인데 현장이 들어온 것이 없다\n * ③은 특히 나쁘다 — 커넥터의 매핑이 한 글자 틀리면 트윈은 「빈 공장」이 되고, 그 사실이 서버 로그\n * 한 줄에만 남았다. 현장에 나간 사람은 로그를 뒤져야 했다.\n *\n * ── 왜 저널로는 답할 수 없나 ────────────────────────────────────────────────\n * **거부된 것은 저널에 없다** — 거부의 뜻이 그것이다. 저널을 계산하면 통과한 쪽만 나온다. 거부의 추이는\n * 세는 자리를 따로 두어야 나온다. 이 장부가 그 자리다.\n *\n * ── 왜 건마다 저장하지 않나 ─────────────────────────────────────────────────\n * 라이브 계측은 초당 여러 건이다. 거부마다 행을 쓰면 그 자체가 부하가 된다(`reference-live.ts` 가\n * 이미 그렇게 적어 두었다). 그리고 이 패키지는 **`integration-base` 를 알지 않는다**(이식성 규약) —\n * 시나리오 경로가 쓰는 dead-letter 를 여기서 부를 수 없다. 그래서 **집계한다.**\n *\n * ── 통과와 거부를 함께 센다 ─────────────────────────────────────────────────\n * 거부만 그리면 「유입 없음」과 「정상」이 **둘 다 거부 0** 으로 같아 보인다. 제시 수가 있어야 갈린다.\n *\n * ── 지키는 규율 넷 ──────────────────────────────────────────────────────────\n * ① **닫힌 창만 「최근」이다** — 채우는 중인 창은 과소 집계다(부하 계기판에서 겪었다).\n * ② **0 과 「없음」을 가른다** — 안 받은 것은 `null`, 받았지만 거부 0 은 `0`.\n * ③ **알리지 않고 자르지 않는다** — 표본·추이 상한에서 밀려난 수를 함께 낸다.\n * ④ **통과율에 1 을 함부로 쓰지 않는다** — 1,000건 중 1건 거부는 0.999 다.\n */\n\n/** 추이 창 하나 — 제시·통과·거부·미전달, 그리고 **적힌 것**을 함께 든다. */\nexport interface IngestWindow {\n /** 창이 열린 시각(ms). */\n startMs: number\n /** 창이 닫힌 시각(ms). 아직 채우는 중이면 `-1`. */\n endMs: number\n /** 이 창에 제시된 레코드 수(거부 여부 무관). */\n offered: number\n /** 그중 **매핑이 거부한** 수 — 처방은 매핑을 고치는 것이다. */\n rejected: number\n /**\n * 매핑은 통과했지만 **트윈에 닿지 못한** 수 — 처방은 트윈을 세우는 것이다.\n *\n * ── 왜 거부와 따로 세나 (2026-08-20에 겪었다) ─────────────────────────────\n * 트윈이 라이브로 돌지 않으면 커널이 레코드를 **알리지 않고 버린다**. 그런데 장부는 그것을 「통과」로 셌다.\n * 화면이 「150 들어옴 · 150 통과 · 100%」라고 말하는 동안 150건은 바닥에 버려지고 있었다. 이 기능이\n * 막으려던 바로 그것을 이 기능이 저지른 셈이다.\n *\n * 거부에 합치는 것도 답이 아니다 — 거부는 「매핑을 고쳐라」는 말이고, 여기서 필요한 말은 「트윈이 안\n * 돌고 있다」다. **처방이 다르면 다른 수로 센다.**\n */\n undelivered: number\n /**\n * 이 창에 **저널에 적힌 행** 수 — 「무엇이 남았나」.\n *\n * 유입과 다른 축이다: 시뮬 트윈은 받는 것 없이 자기가 낸 사실을 적는다(그래서 `offered` 는 0 이고\n * `rows` 는 0 이 아니다). 이 칸이 없으면 그 트윈의 활동은 어느 추이에도 나타나지 않는다.\n */\n rows: number\n /**\n * 그것을 적기 위해 **DB 를 두드린 횟수**(flush) — 「우리가 몇 번 갔나」.\n *\n * 행 수와 따로 세는 이유: 2026-08-20 에 고친 것이 바로 이 값이다(행마다 한 번 → 창당 한 번). 행만\n * 세면 그 개선도, 되돌아가는 것도 보이지 않는다.\n */\n flushes: number\n}\n\n/** 거부 표본 하나 — 매핑을 고치려면 실제 모양을 봐야 한다. */\nexport interface RejectSample {\n atMs: number\n /** 커널이 낸 사유. 우리가 다시 쓰지 않고 그대로 옮긴다. */\n errors: string[]\n /** 막힌 레코드의 직렬화. 길면 자르고, 잘랐다고 표시한다. */\n record: string\n recordTruncated?: boolean\n}\n\nexport interface IngestLedger {\n /** 지금 채우는 중인 창. `startMs < 0` 이면 아직 아무것도 받지 않았다. */\n window: IngestWindow\n /** 닫힌 창들 — 오래된 것이 앞. 상한을 넘으면 앞에서 밀려난다. */\n trend: IngestWindow[]\n /** 추이에서 밀려난 창 수. */\n trendDropped: number\n /** 누적 제시·거부·미전달. */\n offered: number\n rejected: number\n undelivered: number\n /** 누적 — 적힌 행과 두드린 횟수. */\n rows: number\n flushes: number\n /**\n * 마지막으로 **적은** 시각. 한 번도 없으면 `-1`.\n *\n * 유입 시각(`lastAtMs`)과 갈라 두는 이유는 위 `recordJournalWrite` 주석에 있다 — 섞으면 끊긴 피드가\n * 초록으로 보인다.\n */\n lastWriteAtMs: number\n /**\n * 마지막으로 **버려진** 시각(ms). 한 번도 없으면 `-1`.\n *\n * 창과 따로 둔다: 미전달은 창이 닫히기 전에 알아야 한다(그 사이에도 사실은 계속 사라지고 있다).\n */\n lastUndeliveredAtMs: number\n /** 사유별 누적 건수와 **마지막으로 그 사유가 난 시각** — 고친 사유는 시각이 멈춘다. */\n /**\n * **어떤 길로 들어왔나** — 경로마다 건수와 마지막 시각.\n *\n * ── 왜 필요한가 (2026-08-26) ──────────────────────────────────────────────\n * 트윈이 값을 받고 있다는 것은 계기가 말해 주는데, **무엇을 통해 받는지는 아무도 말하지 않았다.**\n * 커넥터가 물어서 받는지, 연결된 시스템이 밀어 주는지, 시나리오가 넣는지, 부하 도구가 넣는지 —\n * 화면에서 구별할 수 없었다. 그 구별이 없으면 「폴링을 웹훅으로 바꿨다」를 확인할 방법도 없다.\n */\n bySource: Record<string, { count: number; lastAtMs: number }>\n /**\n * **같은 사실이 두 번 와서 버린 수** — 알리지 않고 버리지 않는다.\n *\n * 이 수가 크면 보내는 쪽이 되풀이 보내고 있다는 뜻이고, 그것은 고칠 일이다(태양광 커넥터가 한 주기에\n * 76건을 되풀이 보내던 것을 이 수로 볼 수 있었어야 했다).\n */\n duplicates: number\n lastDuplicateAtMs: number\n byReason: Record<string, { count: number; lastAtMs: number }>\n /**\n * 사유 종류가 상한에 닿았나.\n *\n * **모은 종류 수는 세지 않는다** — 그것을 세려면 이미 본 사유를 모두 기억해야 하고, 그 집합이\n * 바로 이 상한으로 막으려던 무한한 집합이다. 모은 **건수**는 `byReason[OTHER_REASON].count` 가 답한다.\n */\n reasonsCapped: boolean\n /** 표본을 이미 뜬 사유들 — 같은 사유를 다시 직렬화하지 않기 위한 색인(O(1) 조회). */\n sampledReasons: Set<string>\n /** 최근 거부 표본. */\n samples: RejectSample[]\n /** 표본에서 밀려난 거부 건수. */\n sampleDropped: number\n /** 마지막으로 무엇이든 받은 시각(ms). **한 번도 없으면 `-1`** — 0 으로 두면 시각 0 과 구별되지 않는다. */\n lastAtMs: number\n /**\n * **원본에 닿지 못한 사실** (2026-08-23) — 「받은 것이 없다」와 **다른 사실**이다.\n *\n * ── 무엇이 아무 신호도 내지 않았나 ─────────────────────────────────────────────────────────\n * 실 원본이 끊겼을 때(접속 시간 초과) 트윈의 **조회 가능한 상태 어디에도** 그 사실이 없었다.\n * `recordIngest` 는 `onRecords` 가 불릴 때만 도달하고, 읽기가 실패하면 그 문에 오지 않는다.\n * `TwinReference.lastError` 는 마스터 동기·접속 시험에만 적힌다.\n *\n * 그래서 화면이 볼 수 있는 것은 「새 사실이 없다」뿐이었고, 그것은 **「연결된 시스템에서 들어온 것이 없다」와 구별되지\n * 않는다.** 실증 데모 중에 원본이 끊기면 사용자가 원인을 찾을 수 없다.\n *\n * ── 왜 「몇 번」과 「언제부터」를 함께 두나 ──────────────────────────────────\n * 한 번 실패한 것과 20분째 못 읽는 것은 다른 사실이다. 그 구별이 없으면 화면이 순간의 실패에 놀라거나\n * 계속된 단절을 대수롭지 않게 말한다. 그래서 **연속 실패 수**와 **처음 실패한 시각**을 함께 든다.\n *\n * 성공하면 지운다 — 고쳐진 단절이 화면에 남아 있으면 그것도 거짓이다.\n */\n /**\n * 마지막으로 끊겼다가 돌아온 것 — 없으면 끊긴 적이 없거나 아직 돌아오지 않은 것이다.\n *\n * 실패 기록(`readFailure`)은 돌아오면 지워진다. 그것만 지우면 「끊긴 적이 있었다」가 화면에서\n * 사라지므로, 돌아온 사실을 따로 남긴다.\n */\n lastRecovery?: { atMs: number; afterFailures: number; downMs: number }\n readFailure?: {\n /** 연속 실패 수 — 성공 한 번에 0 으로 돌아간다. */\n consecutive: number\n /** 이 단절이 **처음** 시작된 시각(ms). 연속이 끊기면 다시 정해진다. */\n sinceMs: number\n /** 마지막 실패 시각(ms). */\n lastAtMs: number\n /** 마지막 실패의 사유 — 사람이 읽는 한 줄(원본이 준 말 그대로). */\n reason: string\n /** 어느 흐름에서 났나(어댑터가 말해 주면). 원본마다 흐름이 다르므로 열려 있다. */\n stream?: string\n /** 다음에 다시 물을 때까지 남은 시간(ms) — 커넥터가 물러섰을 때만 있다. */\n nextRetryMs?: number\n }\n /**\n * **읽었는데 창을 넘길 수 없다** (2026-08-24) — 위와 **다른 사실**이다.\n *\n * ── 왜 갈랐나 ───────────────────────────────────────────────────────────────\n * 이 칸을 만들기 전에는 두 사실이 같은 이름으로 나갔다.\n *\n * readFailure 원본에 닿지 못했다 → **기다리면 풀린다**\n * cursorStall 읽었는데 커서가 못 넘어간다 → **기다려도 안 풀린다**\n *\n * 둘째는 읽기가 성공한 실패다: 한 시각에 한 페이지보다 많은 행이 몰려 커서를 그 시각 밖으로 옮길 수\n * 없다. 화면이 「원본에 닿지 못한다」고 말하면 사람은 원본을 의심하고 기다리는데, 필요한 조치는\n * 페이지를 키우거나 같은 시각 안에서 순서를 정하는 것이다. **조치가 반대인 두 사실을 한 이름으로\n * 부르면 그 이름은 정보가 아니라 오해다.**\n *\n * 모양은 위와 같게 둔다 — 「몇 번」과 「언제부터」가 여기서도 같은 값을 한다(한 번의 밀림과 사흘째\n * 못 넘어가는 것은 다르다). 넘어가면 지운다.\n */\n /**\n * **원본이 말했는데 트윈에 세우지 않은 것** — 이유별로 센다.\n *\n * ── 왜 이 자리가 필요한가 (2026-08-24) ─────────────────────────────────────\n * 커넥터가 원본의 사실 일부를 **일부러 받지 않는 일이 정상이다**: 입자가 맞지 않거나, 받으면 상태가\n * 거짓이 되거나, 커널 어휘로 옮길 수 없다. 그때 커넥터는 경고를 낸다 — **그런데 그 경고가 프로비저닝\n * 화면에서 한 번 스쳐 지나갈 뿐이었다.** 그 뒤로는 어디에서도 볼 수 없었다.\n *\n * 그래서 사용자가 「원본보다 계획이 적다」를 물으면 **답이 어디에도 없다.** 그 사실은 한 번의\n * 사건이 아니라 **상태의 성질**이다 — 세우지 않은 것은 지금도 트윈에 없다.\n *\n * ── `unhandled` 의 형제다 ──────────────────────────────────────────────────\n * `unhandled` 리듀서가 **받았는데** 담을 자리를 몰랐다\n * 여기 커넥터가 **보지만 일부러 보내지 않았다** — 이유와 함께\n *\n * 둘 다 「사실이 오류 없이 사라지지 않게」 세는 것이고, 이유가 다르므로 자리도 다르다.\n *\n * 이유를 **문자열로 받는다**: 무엇을 왜 안 받는지는 원본마다 다르고, 계약이 그 목록을 닫으면 새 원본을\n * 붙일 때마다 계약을 고치게 된다(흐름 열쇠와 같은 규율).\n */\n withheld?: { reason: string; count: number; lastAtMs: number }[]\n cursorStall?: {\n /** 연속 정체 수 — 넘어가면 0 으로 돌아간다. */\n consecutive: number\n /** 이 정체가 **처음** 시작된 시각(ms). */\n sinceMs: number\n /** 마지막 정체 시각(ms). */\n lastAtMs: number\n /** 사유 — 사람이 읽는 한 줄(어느 시각에서 막혔나까지 어댑터가 말해 주면 좋다). */\n reason: string\n /** 어느 흐름인가. 밀도가 높은 표는 원본마다 다르므로 이 이름이 조치의 절반이다. */\n stream?: string\n }\n}\n\n/**\n * 읽기 실패를 장부에 적는다 — **받은 것이 없는 것과 가른다.**\n *\n * 연속 실패의 시작 시각을 지키는 것이 이 함수의 요점이다: 매번 새로 적으면 「20분째 못 읽는다」를\n * 말할 수 없고, 화면은 늘 「방금 실패했다」만 보게 된다.\n */\nexport function recordReadFailure(\n ledger: IngestLedger,\n reason: string,\n nowMs: number,\n stream?: string,\n /**\n * 다음에 다시 물을 때까지 남은 시간(ms) — 커넥터가 물러섰으면 그 값을 준다 (2026-08-27).\n *\n * 이것이 없으면 화면이 「닿지 못하고 있다」까지만 말하고, 사람은 **언제 다시 시도하는지** 알 수\n * 없다. 그러면 기다려야 하는지 손을 써야 하는지 판단하지 못한다.\n *\n * **연달아 횟수와 시작 시각은 커넥터가 보내지 않는다** — 이 함수가 이미 센다. 두 곳에서 세면\n * 갈라지고, 갈라지면 어느 쪽이 사실인지 알 수 없다.\n */\n nextRetryMs?: number\n): void {\n const prev = ledger.readFailure\n ledger.readFailure = {\n consecutive: (prev?.consecutive ?? 0) + 1,\n sinceMs: prev?.sinceMs ?? nowMs,\n lastAtMs: nowMs,\n reason,\n ...(stream ? { stream } : {}),\n ...(Number.isFinite(nextRetryMs) && (nextRetryMs as number) > 0 ? { nextRetryMs: nextRetryMs as number } : {})\n }\n}\n\n/**\n * **끊겼다가 돌아왔다** — 그 사실을 남긴다 (2026-08-27).\n *\n * 실패 기록만 지우면 「끊긴 적이 있었다」가 화면에서 사라진다. 그러면 밤새 두 시간 끊겼던 연결과 한\n * 번도 끊기지 않은 연결이 아침에 똑같이 보인다. 몇 번 실패한 뒤에 돌아왔는지, 얼마나 끊겼는지를 함께\n * 남긴다.\n */\nexport function recordRecovered(ledger: IngestLedger, afterFailures: number, downMs: number, nowMs: number): void {\n if (ledger.readFailure) delete ledger.readFailure\n ledger.lastRecovery = { atMs: nowMs, afterFailures, downMs }\n}\n\n/**\n * 읽기가 성공했다 — 단절 기록을 지운다.\n *\n * **빈 읽기도 성공이다**: 원본이 「새 것이 없다」고 답한 것은 닿았다는 뜻이다. 그 둘을 같게 두면\n * 들어온 것이 없는 연결이 끊긴 원본으로 보인다(고치려던 것의 반대 방향으로 틀린다).\n */\nexport function clearReadFailure(ledger: IngestLedger): void {\n if (ledger.readFailure) delete ledger.readFailure\n}\n\n/**\n * 커서 정체를 장부에 적는다 — **닿지 못한 것과 가른다.**\n *\n * `recordReadFailure` 와 같은 모양이지만 **다른 칸**이다. 한 주기가 두 이유로 실패할 수는 없으므로\n * (먼저 닿아야 읽는다) 둘이 동시에 서지 않는다 — 그래서 화면은 둘 중 하나만 보게 되고, 그 하나가\n * 조치를 정한다.\n */\nexport function recordCursorStall(ledger: IngestLedger, reason: string, nowMs: number, stream?: string): void {\n const prev = ledger.cursorStall\n ledger.cursorStall = {\n consecutive: (prev?.consecutive ?? 0) + 1,\n sinceMs: prev?.sinceMs ?? nowMs,\n lastAtMs: nowMs,\n reason,\n ...(stream ? { stream } : {})\n }\n}\n\n/**\n * **세우지 않은 것을 적는다** — 이유별 누적.\n *\n * 주기마다 다시 알려도 시끄러워지지 않게 **이유로 묶어 센다**(줄이 늘지 않는다). 그리고 마지막 시각을\n * 함께 든다 — 「지금도 그런가」와 「한때 그랬나」는 다른 사실이고, 시각 없이는 구별할 수 없다.\n *\n * 이유가 같고 수가 달라지면 **마지막 수로 덮는다**: 이것은 누적 사건이 아니라 **지금 세우지 않은 것의\n * 수**다. 주기마다 8건이면 8이고, 더해서 800이 되면 거짓이다.\n */\n/**\n * 어떤 길로 몇 건이 들어왔나 — 경로 이름은 부르는 쪽이 정한다.\n *\n * 이름을 자유 문자열로 두는 이유: 길이 늘어난다(지금은 물어보기·시나리오·부하 도구, 다음은 웹훅).\n * 열거로 못 박으면 새 길이 생길 때 이 파일을 함께 고쳐야 하고, 잊으면 그 길이 통계에서 사라진다.\n */\nexport function recordIngestSource(ledger: IngestLedger, source: string, count: number, nowMs: number): void {\n const key = String(source ?? '').trim() || 'unknown'\n if (count <= 0) return\n const cur = (ledger.bySource[key] ??= { count: 0, lastAtMs: -1 })\n cur.count += count\n cur.lastAtMs = nowMs\n}\n\n/** 같은 사실이 두 번 와서 버린 수 — 알리지 않고 버리지 않는다. */\nexport function recordDuplicates(ledger: IngestLedger, count: number, nowMs: number): void {\n if (count <= 0) return\n ledger.duplicates += count\n ledger.lastDuplicateAtMs = nowMs\n}\n\nexport function recordWithheld(ledger: IngestLedger, reason: string, count: number, nowMs: number): void {\n if (!reason || !(count > 0)) return\n const list = (ledger.withheld ??= [])\n const at = list.find(w => w.reason === reason)\n if (at) {\n at.count = count\n at.lastAtMs = nowMs\n return\n }\n list.push({ reason, count, lastAtMs: nowMs })\n}\n\n/** 창을 넘겼으면 지운다 — 풀린 정체가 화면에 남아 있으면 그것도 거짓이다. */\nexport function clearCursorStall(ledger: IngestLedger): void {\n if (ledger.cursorStall) delete ledger.cursorStall\n}\n\n/** 창 길이 — 10분. 부하 계기판(10초)과 다른 값이라 이름을 따로 둔다(같은 이름이면 섞인다). */\nexport const INGEST_WINDOW_MS = 600_000\n\n/**\n * 이만큼 아무것도 오지 않으면 「유입 없음」이라 말한다.\n *\n * 창 하나와 같게 둔다 — 한 창을 통째로 비운 것은 실제로 「그 구간에 아무것도 오지 않았다」이고, 추이의\n * 그 자리도 비게 된다. 그림과 알약이 같은 말을 하게 하려면 같은 길이여야 한다.\n */\nexport const STALE_AFTER_MS = INGEST_WINDOW_MS\n\n/** 추이 보관 창 수 — 36창 × 10분 = 6시간. 「고친 것이 먹혔나」를 보기에 충분하고 메모리는 숫자 넷×36 이다. */\nexport const TREND_LIMIT = 36\n\n/**\n * 사유 종류 상한.\n *\n * **없으면 메모리가 끝없이 자란다.** 사유 문구에 레코드마다 다른 값이 섞이는 커넥터가 하나만 있어도\n * (`unknown bizStep \"PUTAWAY_X17\"`, `…X18`, …) 키가 무한히 늘어난다. 초당 1만 건 규모에서 이것은\n * 미미한 오버헤드가 아니라 누수다.\n *\n * 넘치면 `OTHER_REASON` 한 칸에 모으고 **몇 종을 모았는지 함께 낸다**(알리지 않고 자르지 않는다).\n * 30종이면 고칠 곳을 찾기에 충분하다 — 목록을 훑는 일이 아니라 매핑 몇 줄을 고치는 일이다.\n */\nexport const REASON_LIMIT = 30\n\n/** 상한을 넘긴 사유들이 모이는 칸. */\nexport const OTHER_REASON = '(other reasons)'\n\n/**\n * 표본 상한 — **사유마다 한 건**만 든다.\n *\n * 같은 사유 20건보다 **서로 다른 사유 1건씩**이 매핑을 고치는 데 유용하다. 그리고 이 규칙이 성능을\n * 지킨다: 그 사유의 표본이 이미 있으면 `JSON.stringify` 를 **아예 하지 않는다.** 실측(레코드 241바이트)\n * 으로 건당 0.24µs 인데, 초당 1만 건이 전부 거부되면 9,980건은 만들자마자 버려지는 문자열이었다 —\n * CPU 는 감당되지만 GC 압박이 이벤트 루프로 돌아온다.\n */\nexport const SAMPLE_LIMIT = REASON_LIMIT\n\n/** 표본 하나의 직렬화 상한(문자). */\nexport const RECORD_MAX = 2000\n\n/**\n * 매핑이 막고 있다고 볼 통과율 문턱.\n *\n * 값과 함께 내보낸다 — 화면이 「95% 미만이라 매핑 문제로 봤다」를 말할 수 있게. 숨긴 문턱은 사람이\n * 판정을 믿지 못한다.\n */\nexport const HEALTHY_ACCEPTED_RATIO = 0.95\n\nexport function newIngestLedger(): IngestLedger {\n return {\n window: { startMs: -1, endMs: -1, offered: 0, rejected: 0, undelivered: 0, rows: 0, flushes: 0 },\n trend: [],\n trendDropped: 0,\n offered: 0,\n rejected: 0,\n undelivered: 0,\n rows: 0,\n flushes: 0,\n lastWriteAtMs: -1,\n lastUndeliveredAtMs: -1,\n bySource: {},\n duplicates: 0,\n lastDuplicateAtMs: -1,\n byReason: {},\n reasonsCapped: false,\n sampledReasons: new Set<string>(),\n samples: [],\n sampleDropped: 0,\n lastAtMs: -1\n }\n}\n\n/**\n * 창이 다 찼으면 닫아 추이에 넣고 새로 연다. **닫힌 창만 「최근」으로 읽는다.**\n *\n * ── 영속이 붙는 유일한 자리 ────────────────────────────────────────────────\n * 닫힌 창 하나가 영속의 단위다(숫자 넷 + 구간). `onClosed` 를 받는 이유는 **이 함수가 창이 닫히는\n * 유일한 지점**이기 때문이다 — 여기 한 곳에 붙이면 기록 경로가 갈리지 않는다.\n *\n * 왜 건마다가 아니라 창마다인가: 거부마다 행을 쓰면 초당 1만 행이고 그 부하가 이벤트 루프로 돌아온다.\n * 창마다면 트윈 1,000개에 하루 14.4만 행(약 14MB)이다.\n *\n * `onClosed` 는 **오류를 내지 않아야 한다**(호출부가 감싼다) — 영속이 실패해도 장부는 계속 굴러야 한다.\n * 영속을 지키는 것과 관측을 지키는 것은 다른 일이고, 뒤엣것이 앞엣것 때문에 멈추면 안 된다.\n */\nexport function rollIngestWindow(\n ledger: IngestLedger,\n nowMs: number,\n windowMs = INGEST_WINDOW_MS,\n onClosed?: (closed: IngestWindow) => void\n): void {\n if (ledger.window.startMs < 0) {\n ledger.window.startMs = nowMs\n return\n }\n if (nowMs - ledger.window.startMs < windowMs) return\n\n const closed: IngestWindow = { ...ledger.window, endMs: nowMs }\n ledger.trend.push(closed)\n ledger.window = { startMs: nowMs, endMs: -1, offered: 0, rejected: 0, undelivered: 0, rows: 0, flushes: 0 }\n\n if (ledger.trend.length > TREND_LIMIT) {\n const drop = ledger.trend.length - TREND_LIMIT\n ledger.trend.splice(0, drop)\n ledger.trendDropped += drop\n }\n\n /* 영속은 관측을 막지 않는다 — 터져도 장부는 이미 갱신됐다. */\n if (onClosed) {\n try {\n onClosed(closed)\n } catch {\n /* 호출부가 알린다. 여기서 삼키는 것은 **장부의 진행**을 지키기 위해서다. */\n }\n }\n}\n\n/** 사유 묶음의 키. 사유가 여럿이면 한 줄로 잇고, 없으면 지어내지 않고 `unknown`. */\nexport function reasonKey(errors: string[] | undefined): string {\n const list = (errors ?? []).map(e => String(e).trim()).filter(Boolean)\n return list.length ? list.join(' · ') : 'unknown'\n}\n\nfunction safeJson(v: unknown): string {\n try {\n const s = JSON.stringify(v)\n return s === undefined ? String(v) : s\n } catch {\n return '(serialization failed)' /* 순환 참조 등 — 모양을 못 보여 주는 것도 사실이다 */\n }\n}\n\n/**\n * 한 번의 인제스트 결과를 적는다.\n *\n * `offered` 를 함께 받는 이유: 통과율을 **서로 다른 두 계수기**에서 나눠 계산하면 분모와 분자가 다른\n * 것을 세게 된다. 둘을 한자리에서 본 수를 그대로 적는다.\n */\nexport function recordIngest(\n ledger: IngestLedger,\n offered: number,\n rejected: { record?: unknown; errors?: string[] }[] | undefined,\n nowMs: number,\n onClosed?: (closed: IngestWindow) => void,\n /** 매핑은 통과했지만 트윈에 닿지 못한 수 — 「통과」로 세면 화면이 거짓을 말한다. */\n undelivered = 0\n): void {\n if (!Number.isFinite(offered) || offered < 0) return\n\n rollIngestWindow(ledger, nowMs, INGEST_WINDOW_MS, onClosed)\n\n ledger.offered += offered\n ledger.window.offered += offered\n ledger.lastAtMs = nowMs\n\n if (Number.isFinite(undelivered) && undelivered > 0) {\n ledger.undelivered += undelivered\n ledger.window.undelivered += undelivered\n ledger.lastUndeliveredAtMs = nowMs\n }\n\n const items = rejected ?? []\n if (!items.length) return\n\n ledger.rejected += items.length\n ledger.window.rejected += items.length\n\n for (const it of items) {\n const raw = reasonKey(it?.errors)\n const known = raw in ledger.byReason\n\n /* 사유 종류 상한 — 넘치면 한 칸으로 모으고, **닿았다는 사실**을 표시한다. */\n let key = raw\n if (!known && Object.keys(ledger.byReason).length >= REASON_LIMIT) {\n key = OTHER_REASON\n ledger.reasonsCapped = true\n }\n\n const seen = ledger.byReason[key]\n ledger.byReason[key] = { count: (seen?.count ?? 0) + 1, lastAtMs: nowMs }\n\n /*\n * **표본은 사유마다 하나.** 이미 그 사유의 표본이 있으면 직렬화를 아예 하지 않는다 — 초당 1만 건이\n * 전부 거부될 때 만들자마자 버려지는 문자열을 만들지 않기 위해서다(실측: 건당 0.24µs, GC 압박).\n */\n if (ledger.sampledReasons.has(key)) {\n ledger.sampleDropped += 1\n continue\n }\n\n const text = safeJson(it?.record)\n const truncated = text.length > RECORD_MAX\n ledger.samples.push({\n atMs: nowMs,\n errors: key === OTHER_REASON ? [OTHER_REASON] : it?.errors?.length ? [...it.errors] : ['unknown'],\n record: truncated ? text.slice(0, RECORD_MAX) : text,\n ...(truncated ? { recordTruncated: true } : {})\n })\n\n ledger.sampledReasons.add(key)\n\n if (ledger.samples.length > SAMPLE_LIMIT) {\n const gone = ledger.samples.splice(0, ledger.samples.length - SAMPLE_LIMIT)\n /* 밀려난 표본의 사유는 색인에서도 빼야 다음에 다시 뜰 수 있다(안 빼면 그 사유는 영원히 표본 없음). */\n for (const g of gone) ledger.sampledReasons.delete(reasonKey(g.errors))\n ledger.sampleDropped += gone.length\n }\n }\n}\n\n/**\n * 저널에 **적은 것**을 기록한다 — 창을 넘길 때가 되었으면 넘기고, 행·왕복을 더한다.\n *\n * ── 왜 유입과 같은 창인가 ───────────────────────────────────────────────────\n * 새 장부를 만들지 않는다. 이 창은 「닫힌 창만 최근」·「0 과 없음을 가른다」·「잘린 것을 세어 드러낸다」를\n * 이미 지키고, 닫힐 때 영속까지 된다. 같은 규율을 두 벌 만들면 한쪽만 고쳐진다.\n *\n * ── 유입이 없어도 창은 넘어간다 ────────────────────────────────────────────\n * 시뮬 트윈은 받는 것 없이 자기가 낸 사실을 적는다. 창이 **유입에서만** 넘어간다면 그 트윈은 추이에\n * 영원히 나타나지 않는다 — 그래서 쓰기도 창을 넘긴다.\n */\nexport function recordJournalWrite(\n ledger: IngestLedger,\n rows: number,\n nowMs: number,\n onClosed?: (closed: IngestWindow) => void\n): void {\n if (!Number.isFinite(rows) || rows <= 0) return\n rollIngestWindow(ledger, nowMs, INGEST_WINDOW_MS, onClosed)\n ledger.rows += rows\n ledger.flushes += 1\n ledger.window.rows += rows\n ledger.window.flushes += 1\n /*\n * **`lastAtMs` 는 건드리지 않는다.** 그 값은 「마지막 유입」이고, 끊긴 피드를 가려내는 근거다. 쓰기로\n * 갱신하면 라이브 트윈은 유입이 끊겨도 틱이 만든 델타를 계속 적으므로 **영원히 「방금 받았다」**가 되고,\n * 「오래 끊긴 피드에 옛 창으로 초록을 내지 않는다」가 오류 하나 없이 무력해진다. 축을 따로 든다.\n */\n ledger.lastWriteAtMs = nowMs\n}\n\n/**\n * 한눈 판정.\n *\n * `not-applicable` 과 `not-connected` 를 섞지 않고, 재지 않은 것을 정상·비정상으로 채우지 않는다\n * (`unknown`). 시뮬 트윈에 「동기화 안 됨」이라 말하면 거짓 경보이고, **거짓 경보는 참 경보를 죽인다.**\n */\nexport type SyncVerdict =\n | 'syncing' /** 유입이 있고 통과율이 문턱 이상 */\n | 'mapping-blocked' /** 유입은 있는데 거부가 문턱을 넘는다 — 사유를 보고 매핑을 고친다 */\n /**\n * 들어오는데 **커널이 받지 않는다** — 사실이 사라지고 있다.\n *\n * ── 이름이 사실보다 좁다 (2026-08-20에 드러났다) ────────────────────────────\n * 관측한 것은 「커널이 받지 않았다」(`ingestLive` 가 0)이고, **멈춘 것은 그 원인 중 하나일 뿐**이다.\n * 다른 원인이 실제로 있었다: 미러로 선언된 트윈을 사람이 화면에서 시작하면 시뮬 구동으로 떴고, 그\n * 트윈은 **돌고 있으면서** 유입을 한 건도 받지 못했다. 그때 화면은 한쪽에 「running」, 다른 쪽에\n * 「멈춰」라고 적었고 사람은 「시작 버튼을 누르면 되겠네」라는 엉뚱한 처방으로 갔다.\n *\n * 그래서 **화면 문구는 관측 그대로** 적는다(「트윈에 닿지 않고 버려지는 중」). 이 키 이름은 서버·화면·\n * 시험에 걸친 계약 문자열이라 그대로 두지만, 이름을 원인으로 읽지 않는다.\n */\n | 'twin-not-running'\n | 'no-traffic' /** 피드는 붙었는데 제시가 0 — 원천·망을 본다 */\n | 'not-connected' /** 피드 자체가 없다 — 결선한다 */\n | 'not-applicable' /** 원본이 먹이지 않는 트윈(시뮬)·멈춘 트윈 — 물을 대상이 아니다 */\n | 'unknown' /** 한 번도 받지 않았거나 창이 아직 닫히지 않았다 — 기다린다 */\n\nexport function syncVerdictOf(args: {\n /** `live-feed-registry.liveFeedStateOf` 의 답을 그대로 받는다 — 판정 어휘를 두 벌 만들지 않는다. */\n feedState: 'attached' | 'detached' | 'not-applicable'\n /** 마지막으로 **닫힌** 창. 없으면 `null`. */\n recent: { offered: number; rejected: number } | null\n /** 마지막으로 무엇이든 받은 시각. 한 번도 없으면 `-1` — 0 이 아니다(0 은 1970년이다). */\n lastAtMs?: number\n /** 지금. `lastAtMs` 를 견줄 때만 쓴다. */\n nowMs?: number\n /** 마지막으로 **버려진** 시각. 한 번도 없으면 `-1`. */\n lastUndeliveredAtMs?: number\n /**\n * 지금까지 제시된 누적 수 — **「받고 있는 중」과 「오는 것이 없다」를 가르는 사실**이다.\n *\n * 닿은 시각(`lastAtMs`)으로는 가를 수 없다: 빈 읽기도 닿은 것이므로 그 시각이 선다. 창이 아직 닫히지\n * 않았을 때 이 수가 0 이면 오는 것이 없는 것이고, 0 이 아니면 받고 있는 중이다.\n */\n offeredTotal?: number\n threshold?: number\n}): SyncVerdict {\n /*\n * ── 버려지고 있는 것이 가장 먼저다 ────────────────────────────────────────\n * 트윈이 멈추면 피드는 남아 계속 레코드를 나르고, 커널은 그것을 알리지 않고 버린다. 그 상태를 「해당 없음」\n * 이라 부르면 **사실이 사라지는 동안 화면이 안심시킨다** — 실제로 그렇게 떴다(멈춘 트윈 옆에 「150\n * 통과 · 100%」). 그래서 시뮬·멈춤 판정보다 먼저 본다.\n *\n * 시뮬 트윈에는 이 값이 서지 않는다(먹이는 피드가 없으므로 미전달도 없다). 그래서 이 순서가 시뮬에\n * 거짓 경보를 주지 않는다.\n */\n if (\n typeof args.lastUndeliveredAtMs === 'number' &&\n args.lastUndeliveredAtMs >= 0 &&\n typeof args.nowMs === 'number' &&\n args.nowMs - args.lastUndeliveredAtMs <= STALE_AFTER_MS\n ) {\n return 'twin-not-running'\n }\n\n if (args.feedState === 'not-applicable') return 'not-applicable'\n if (args.feedState === 'detached') return 'not-connected'\n\n /*\n * ── 끊긴 피드가 초록으로 남지 않게 ────────────────────────────────────────\n * 창은 **다음 유입이 있을 때** 닫힌다. 그래서 유입이 멈추면 창도 멈추고, 「최근 창」은 멈추기 직전의\n * 건강했던 창으로 남는다. 그 창만 보고 판정하면 **어제 죽은 피드가 오늘도 「동기화 중」**이다. 이\n * 기능이 있는 이유가 바로 그 상태를 드러내는 것이므로, 그때 초록을 내면 목적을 놓친다.\n *\n * 그래서 창과 별개로 **지금까지의 침묵**을 먼저 본다. 한 창을 통째로 아무것도 받지 않았으면 그것은\n * 「유입 없음」이다 — 실제로 그 구간의 창은 비어 있게 된다.\n *\n * 이 판단을 서버에 두는 이유: 소비처가 여럿(목록·연결 상세·앞으로 생길 것)이고, 화면마다 다시\n * 판단하면 규칙이 갈라져 어떤 화면은 초록, 어떤 화면은 경고를 말한다.\n *\n * 한계 — 한 창(10분)보다 **느리게 긷는 원천**은 긷는 사이에 「유입 없음」으로 보인다. 장부는 원천의\n * 주기를 모르므로 이보다 정확히 답할 수 없다. 주기를 알게 되면 그 값으로 견주는 것이 맞다.\n */\n if (typeof args.lastAtMs === 'number' && typeof args.nowMs === 'number') {\n if (args.lastAtMs < 0) return 'unknown' /* 붙었지만 한 번도 받지 않았다 — 기다린다 */\n if (args.nowMs - args.lastAtMs > STALE_AFTER_MS) return 'no-traffic'\n }\n\n /*\n * ── **닿았는데 오는 것이 없다** 를 「모른다」로 답하지 않는다 (2026-08-28) ────\n *\n * 창은 **다음 유입이 있을 때** 닫힌다. 그래서 오는 것이 하나도 없는 트윈은 창이 영원히 닫히지 않고,\n * 이 자리가 `unknown` 을 냈다 — 붙어 있고 37초마다 성공적으로 읽는 트윈이 화면에서 「모른다」였다.\n *\n * 그것이 드러난 계기: 커넥터가 재기동마다 재고 2,665건을 다시 말하던 것을 없앴다. 그러자 정상 상태의\n * 유입이 0 이 되었고, **건강 신호가 그 소음을 타고 있었다**는 것이 보였다.\n *\n * ── 가르는 사실은 **받은 것이 있나**다 (첫 판을 고침) ─────────────────────\n * 처음에는 닿은 시각으로 갈랐다. 그것이 틀렸다 — **빈 읽기도 닿은 것**이라 그 시각은 언제나 선다.\n * 그래서 창을 채우는 중인 트윈(제시 200건)까지 「오는 것이 없다」로 답하게 됐고, 시험이 그것을 잡았다.\n * 받고 있는 중인데 오는 것이 없다고 말하는 것은 **없는 것과 못 본 것을 뒤집는 것**이다.\n *\n * 닿았고 · 제시가 0 오는 것이 없다 `no-traffic`\n * 닿았고 · 제시가 있다 받는 중이고 판정할 창이 아직 없다 `unknown`\n * 한 번도 닿지 않았다 `unknown`(위 `lastAtMs < 0` 갈래)\n */\n if (!args.recent) {\n const touched = typeof args.lastAtMs === 'number' && args.lastAtMs >= 0\n const nothingOffered = (args.offeredTotal ?? 0) <= 0\n return touched && nothingOffered ? 'no-traffic' : 'unknown'\n }\n if (args.recent.offered <= 0) return 'no-traffic'\n\n const ratio = (args.recent.offered - args.recent.rejected) / args.recent.offered\n return ratio >= (args.threshold ?? HEALTHY_ACCEPTED_RATIO) ? 'syncing' : 'mapping-blocked'\n}\n\nexport interface IngestWindowView {\n from: string\n to: string\n offered: number\n /** **트윈에 닿은** 수. 제시 − 거부 − 미전달. */\n accepted: number\n /** 매핑이 거부한 수 — 처방은 매핑을 고치는 것. */\n rejected: number\n /** 매핑은 통과했으나 트윈에 닿지 못한 수 — 처방은 트윈을 세우는 것. */\n undelivered: number\n /** 통과율(0~1). 제시가 없으면 `null` — **1 로 채우면 「전부 통과」로 읽힌다.** */\n acceptedRatio: number | null\n /** 이 창에 적힌 행. */\n rows: number\n /** 그것을 적기 위해 DB 를 두드린 횟수 — 행당 왕복이 줄었는지 보는 값. */\n flushes: number\n}\n\nexport interface IngestHealthView {\n verdict: SyncVerdict\n /** 판정에 쓴 문턱 — 화면이 왜 그 판정인지 말할 수 있게 함께 낸다. */\n threshold: number\n /** 마지막으로 닫힌 창. 창이 한 번도 닫히지 않았으면 `null`. */\n recent: IngestWindowView | null\n /** 닫힌 창들, 오래된 것부터. */\n trend: IngestWindowView[]\n /** 추이 상한에서 밀려난 창 수. */\n trendDropped: number\n /** 누적(기동 이후). */\n total: {\n offered: number\n accepted: number\n rejected: number\n undelivered: number\n acceptedRatio: number | null\n /** 저널에 적힌 행과 그것을 위해 DB 를 두드린 횟수 — 기동 이후 누적. */\n rows: number\n flushes: number\n }\n /** 사유별 누적, 많은 것부터. `lastAt` 이 멈춘 사유는 이미 고쳐진 것이다. */\n reasons: { reason: string; count: number; lastAt: string }[]\n /** 사유 종류가 상한에 닿아 일부가 `(other reasons)` 로 접혔나. */\n reasonsCapped: boolean\n samples: RejectSample[]\n sampleDropped: number\n /** 마지막으로 무엇이든 받은 시각. 한 번도 없으면 `null`. */\n lastAt: string | null\n /** 마지막으로 저널에 적은 시각. 한 번도 없으면 `null` — 받은 적 없는 트윈도 이 값은 있을 수 있다. */\n lastWriteAt: string | null\n /**\n * **원본에 닿지 못하고 있다** — 없으면 닿고 있다는 뜻이다(§`IngestLedger.readFailure`).\n *\n * 커서 정체(`cursorStall`)와 **섞이지 않는다**: 이쪽은 기다리면 풀리고 그쪽은 기다려도 안 풀린다.\n *\n * 「유입 없음」과 **다른 판정**이다: 유입 없음은 연결된 시스템에서 들어온 것이 없는 것일 수 있고, 이것은 우리가 묻지도\n * 못한 것이다. 화면은 두 문장을 다르게 말해야 한다 — 실 원본이 끊겼을 때 사용자가 원인을 찾을 수\n * 있는지가 그 차이다.\n */\n readFailure?: {\n consecutive: number\n /** 이 단절이 시작된 시각 — 「20분째」를 말할 수 있게. */\n since: string\n lastAt: string\n reason: string\n stream?: string\n /** 다음에 다시 물을 때까지 남은 시간(ms) — 커넥터가 물러섰을 때만 있다. */\n nextRetryMs?: number\n }\n /**\n * 마지막으로 끊겼다가 돌아온 것 — 없으면 끊긴 적이 없거나 아직 돌아오지 않은 것이다 (2026-08-27).\n *\n * `readFailure` 는 돌아오면 사라진다. 이 칸이 없으면 밤새 끊겼던 연결과 한 번도 끊기지 않은 연결이\n * 아침에 똑같이 보인다.\n */\n lastRecovery?: { at: string; afterFailures: number; downMs: number }\n}\n\nfunction toWindowView(w: IngestWindow, fallbackEndMs: number): IngestWindowView {\n /* 「통과」는 **트윈에 닿은** 것만이다. 버려진 것을 통과로 세면 화면이 안심시킨다. */\n const accepted = w.offered - w.rejected - w.undelivered\n return {\n from: new Date(w.startMs).toISOString(),\n to: new Date(w.endMs >= 0 ? w.endMs : fallbackEndMs).toISOString(),\n offered: w.offered,\n accepted,\n rejected: w.rejected,\n undelivered: w.undelivered,\n acceptedRatio: w.offered > 0 ? round4(accepted / w.offered) : null,\n rows: w.rows,\n flushes: w.flushes\n }\n}\n\n/**\n * 읽기용 요약.\n *\n * 장부가 없어도(한 번도 받지 않은 트윈) **판정은 낸다** — 피드 사실만으로 답할 수 있는 경우가 있다\n * (`not-applicable`·`not-connected`). 그 경우 수치는 비운다: 없는 값을 0 으로 채우지 않는다.\n *\n * `nowMs` 를 받는 이유: 끊긴 피드를 가려내려면 「마지막 유입이 얼마나 전인가」를 봐야 하고, 그 판단을\n * 화면이 아니라 여기서 한다(소비처마다 다시 판단하면 규칙이 갈라진다). 시험이 시각을 고정할 수 있도록\n * 인자로 받는다.\n */\nexport function ingestHealth(\n ledger: IngestLedger | undefined,\n feedState: 'attached' | 'detached' | 'not-applicable',\n threshold = HEALTHY_ACCEPTED_RATIO,\n nowMs: number = Date.now()\n): IngestHealthView {\n const recentRaw = ledger?.trend.length ? ledger.trend[ledger.trend.length - 1] : null\n /* 열린 창의 끝 시각 대체값 — 유입이 없는 장부에서 `-1` 을 시각으로 읽으면 1969년이 화면에 뜬다. */\n const lastAnyMs = Math.max(ledger?.lastAtMs ?? -1, ledger?.lastWriteAtMs ?? -1)\n const verdict = syncVerdictOf({\n feedState,\n recent: recentRaw,\n lastAtMs: ledger?.lastAtMs ?? -1,\n lastUndeliveredAtMs: ledger?.lastUndeliveredAtMs ?? -1,\n /* 창이 닫히지 않았을 때 「받는 중」과 「오는 것이 없다」를 가르는 사실. */\n offeredTotal: ledger?.offered ?? 0,\n nowMs,\n threshold\n })\n\n /*\n * **유입이 없어도 「빈 장부」가 아니다.** 시뮬 트윈은 받는 것 없이 적는다. 유입 시각만 보고 비었다고\n * 판단하면 그 트윈의 창은 만들어져 있는데 화면에는 아무 추이도 나오지 않는다(이 축을 넣은 이유가 그것이다).\n */\n if (!ledger || (ledger.lastAtMs < 0 && ledger.lastWriteAtMs < 0)) {\n return {\n verdict,\n threshold,\n recent: null,\n trend: [],\n trendDropped: 0,\n total: { offered: 0, accepted: 0, rejected: 0, undelivered: 0, acceptedRatio: null, rows: 0, flushes: 0 },\n reasons: [],\n reasonsCapped: false,\n samples: [],\n sampleDropped: 0,\n lastAt: null,\n lastWriteAt: null,\n /*\n * **이 짧은 답에도 단절은 실어야 한다** — 이 자리가 바로 그 트윈의 모습이다.\n *\n * 받은 것도 적은 것도 없는 장부는 「아직 아무 일도 없다」로 읽히는데, 원본에 닿지 못하는 트윈이\n * 정확히 그 모습이다(읽지 못하면 받은 것도 적은 것도 없다). 여기서 떨어뜨리면 **가장 필요한\n * 순간에** 그 사실이 사라진다 — 실측으로 원본이 끊긴 트윈이 이 경로로 답하고 있었다.\n */\n ...(ledger?.readFailure\n ? {\n readFailure: {\n consecutive: ledger.readFailure.consecutive,\n since: new Date(ledger.readFailure.sinceMs).toISOString(),\n lastAt: new Date(ledger.readFailure.lastAtMs).toISOString(),\n reason: ledger.readFailure.reason,\n ...(ledger.readFailure.stream ? { stream: ledger.readFailure.stream } : {}),\n ...(ledger.readFailure.nextRetryMs ? { nextRetryMs: ledger.readFailure.nextRetryMs } : {})\n }\n }\n : {}),\n ...(ledger?.lastRecovery\n ? {\n lastRecovery: {\n at: new Date(ledger.lastRecovery.atMs).toISOString(),\n afterFailures: ledger.lastRecovery.afterFailures,\n downMs: ledger.lastRecovery.downMs\n }\n }\n : {}),\n /* 커서 정체도 **이 갈래에서** 내보낸다 — 유입이 없는 장부에서도 정체는 실재한다(읽었으나 못\n 넘겼으면 유입이 0이다). 여기서 떨어뜨리면 바로 그 상황에서 들어온 것이 없어진다. */\n ...(ledger?.cursorStall\n ? {\n cursorStall: {\n consecutive: ledger.cursorStall.consecutive,\n since: new Date(ledger.cursorStall.sinceMs).toISOString(),\n lastAt: new Date(ledger.cursorStall.lastAtMs).toISOString(),\n reason: ledger.cursorStall.reason,\n ...(ledger.cursorStall.stream ? { stream: ledger.cursorStall.stream } : {})\n }\n }\n : {}),\n /*\n * **세우지 않은 것도 낸다 — 양쪽 갈래에서.** 유입이 한 건도 없는 장부에서도 이 사실은 실재한다\n * (원본이 말하는데 전부 안 받는 경우가 있다). 여기서 떨어뜨리면 바로 그 상황에서 들어온 것이 없어진다.\n */\n ...(ledger?.withheld?.length\n ? {\n withheld: ledger.withheld.map(w => ({\n reason: w.reason,\n count: w.count,\n lastAt: new Date(w.lastAtMs).toISOString()\n }))\n }\n : {})\n }\n }\n\n /* 「통과」는 트윈에 닿은 것만이다 — 창 뷰와 같은 규칙. */\n const accepted = ledger.offered - ledger.rejected - ledger.undelivered\n return {\n verdict,\n threshold,\n recent: recentRaw ? toWindowView(recentRaw, lastAnyMs) : null,\n trend: ledger.trend.map(w => toWindowView(w, lastAnyMs)),\n trendDropped: ledger.trendDropped,\n /* 세우지 않은 것 — 양쪽 갈래에서 같은 모양으로 낸다(§`IngestLedger.withheld`). */\n ...(ledger.withheld?.length\n ? {\n withheld: ledger.withheld.map(w => ({\n reason: w.reason,\n count: w.count,\n lastAt: new Date(w.lastAtMs).toISOString()\n }))\n }\n : {}),\n /*\n * **어떤 길로 들어오고 있나** — 화면이 「이 트윈은 무엇을 통해 값을 받나」를 말할 수 있게.\n * 비어 있으면 칸을 만들지 않는다: 아직 아무것도 안 받은 트윈과 「길이 없다」를 구별한다.\n */\n ...(Object.keys(ledger.bySource).length\n ? {\n sources: Object.entries(ledger.bySource)\n .map(([source, v]) => ({ source, count: v.count, lastAt: new Date(v.lastAtMs).toISOString() }))\n .sort((a, b) => b.count - a.count)\n }\n : {}),\n /* 같은 사실이 두 번 와서 버린 수 — 0 이면 칸을 만들지 않는다(버린 적이 없다는 뜻이다). */\n ...(ledger.duplicates\n ? { duplicates: { count: ledger.duplicates, lastAt: new Date(ledger.lastDuplicateAtMs).toISOString() } }\n : {}),\n total: {\n offered: ledger.offered,\n accepted,\n rejected: ledger.rejected,\n undelivered: ledger.undelivered,\n acceptedRatio: ledger.offered > 0 ? round4(accepted / ledger.offered) : null,\n rows: ledger.rows,\n flushes: ledger.flushes\n },\n reasons: Object.entries(ledger.byReason)\n .map(([reason, v]) => ({ reason, count: v.count, lastAt: new Date(v.lastAtMs).toISOString() }))\n .sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason)),\n reasonsCapped: ledger.reasonsCapped,\n samples: [...ledger.samples],\n sampleDropped: ledger.sampleDropped,\n /* 「받은 적 없음」과 「방금 받음」을 가른다 — 적기만 하는 트윈의 유입 시각은 **없다**. */\n lastAt: ledger.lastAtMs >= 0 ? new Date(ledger.lastAtMs).toISOString() : null,\n lastWriteAt: ledger.lastWriteAtMs >= 0 ? new Date(ledger.lastWriteAtMs).toISOString() : null,\n /* 닿지 못하고 있으면 그 사실을 낸다 — 없으면 필드를 만들지 않는다(빈 값이 「닿는다」로 읽히지 않게). */\n ...(ledger.readFailure\n ? {\n readFailure: {\n consecutive: ledger.readFailure.consecutive,\n since: new Date(ledger.readFailure.sinceMs).toISOString(),\n lastAt: new Date(ledger.readFailure.lastAtMs).toISOString(),\n reason: ledger.readFailure.reason,\n ...(ledger.readFailure.stream ? { stream: ledger.readFailure.stream } : {}),\n ...(ledger.readFailure.nextRetryMs ? { nextRetryMs: ledger.readFailure.nextRetryMs } : {})\n }\n }\n : {}),\n /* 끊겼다가 돌아온 것 — 지금 닿고 있어도 남긴다(그것이 없으면 밤새의 단절이 사라진다). */\n ...(ledger.lastRecovery\n ? {\n lastRecovery: {\n at: new Date(ledger.lastRecovery.atMs).toISOString(),\n afterFailures: ledger.lastRecovery.afterFailures,\n downMs: ledger.lastRecovery.downMs\n }\n }\n : {}),\n ...(ledger.cursorStall\n ? {\n cursorStall: {\n consecutive: ledger.cursorStall.consecutive,\n since: new Date(ledger.cursorStall.sinceMs).toISOString(),\n lastAt: new Date(ledger.cursorStall.lastAtMs).toISOString(),\n reason: ledger.cursorStall.reason,\n ...(ledger.cursorStall.stream ? { stream: ledger.cursorStall.stream } : {})\n }\n }\n : {})\n }\n}\n\n/** 비율은 자릿수를 넉넉히 준다 — 0.999 가 1 로 반올림되면 「전부 통과」로 읽힌다. */\nconst round4 = (v: number) => Math.round(v * 10000) / 10000\n"]}
@@ -27,7 +27,7 @@ export interface ProbeResult {
27
27
  *
28
28
  * 시뮬레이션 트윈에 라이브 프로브를 들이대면 빨간 칸이 생기는데 그건 결함이 아니다 — 시뮬은 원천에서
29
29
  * 받는 것이 없다. 레퍼런스 없이 템플릿으로 세운 트윈에는 옮겨 온 마스터가 없으므로 「못 옮긴 값」도 없다.
30
- * 이걸 통과로 접으면 없는 능력을 주장하고, 미측정으로 접으면 영원히 배선을 기다린다.
30
+ * 이걸 통과로 처리하면 없는 능력을 주장하고, 미측정으로 처리하면 영원히 배선을 기다린다.
31
31
  */
32
32
  applicable?: boolean;
33
33
  /** 돌렸을 때의 판정. 돌리지 않았으면 무의미하다. */
@@ -43,9 +43,9 @@ export interface ProbeResult {
43
43
  detail?: string;
44
44
  }
45
45
  /**
46
- * 프로브 결과를 **증거**로 접는다.
46
+ * 프로브 결과를 **증거**로 처리한다.
47
47
  *
48
- * 접는 규칙의 정수: **돌리지 않은 프로브는 `null`** 로 간다. 그러면 판정이 그 단계를 달성으로
48
+ * 계산 규칙의 정수: **돌리지 않은 프로브는 `null`** 로 간다. 그러면 판정이 그 단계를 달성으로
49
49
  * 만들지 않고 `unmeasured` 로 말한다 — 스위트를 절반만 돌리고 초록을 주장할 길이 없다.
50
50
  */
51
51
  export declare function foldProbeResults(results: readonly ProbeResult[]): StageEvidence;
@@ -2,11 +2,11 @@
2
2
  /*
3
3
  * 연결 적합성 하네스 — **통합을 시험 스위트처럼 돌린다.**
4
4
  *
5
- * 선언 적합성 하네스 세 벌과 같은 규율을 **연결**에 적용한다: 사실이 조용히 사라지는 것을 막는다.
5
+ * 선언 적합성 하네스 세 벌과 같은 규율을 **연결**에 적용한다: 사실이 오류 없이 사라지는 것을 막는다.
6
6
  * 다른 점은 대상이 우리 선언이 아니라 **남의 시스템**이라는 것이고, 그래서 두 규칙이 더 붙는다.
7
7
  *
8
- * ① **돌리지 않은 프로브는 「실패」가 아니라 「모른다」다.** 실패로 접으면 사람이 그 빨간 칸을
9
- * 없애려고 무엇이든 하게 되고, 모른다로 접으면 **무엇을 아직 모르는지**가 남는다. 판정
8
+ * ① **돌리지 않은 프로브는 「실패」가 아니라 「모른다」다.** 실패로 처리하면 사람이 그 빨간 칸을
9
+ * 없애려고 무엇이든 하게 되고, 모른다로 처리하면 **무엇을 아직 모르는지**가 남는다. 판정
10
10
  * (`assessStagePath`)이 「모른다」를 달성으로 바꾸지 않으므로 이 구분이 끝까지 살아 있다.
11
11
  * ② **쓰기 프로브는 명시 동의 없이 돌지 않는다.** 이 스위트는 고객의 운영 시스템을 두드린다.
12
12
  * 읽기는 기본으로 돌리고, 남의 공장에 명령을 넣는 프로브는 사람이 켜야 돈다 — 켜지 않으면
@@ -42,16 +42,16 @@ exports.PROBE_SEQUENCE = [
42
42
  const by = (rs, id) => rs.find(r => r.id === id);
43
43
  /** 돌렸고 셌으면 그 수, 아니면 `null`(모른다). 실패한 프로브의 수를 0 으로 읽지 않는다. */
44
44
  /**
45
- * 센 값은 **판정과 무관한 사실**이다 — `passed` 를 요구하면 「세어서 0 을 얻었다」가 「모른다」로 접힌다.
45
+ * 센 값은 **판정과 무관한 사실**이다 — `passed` 를 요구하면 「세어서 0 을 얻었다」가 「모른다」와 구별되지 않는다.
46
46
  * 그 구분이 이 하네스의 전부이므로 여기서 잃으면 나머지가 무의미하다. 해당 없음만 `null` 이다.
47
47
  */
48
48
  const counted = (r) => r?.applicable === false ? null : r?.ran && r.count !== undefined ? r.count : null;
49
49
  /** 돌렸고 셌으면 그 수, 아니면 0. **존재 여부**를 묻는 자리에만 쓴다(모름과 0 을 구별할 필요가 없는 축). */
50
50
  const countedOrZero = (r) => counted(r) ?? 0;
51
51
  /**
52
- * 프로브 결과를 **증거**로 접는다.
52
+ * 프로브 결과를 **증거**로 처리한다.
53
53
  *
54
- * 접는 규칙의 정수: **돌리지 않은 프로브는 `null`** 로 간다. 그러면 판정이 그 단계를 달성으로
54
+ * 계산 규칙의 정수: **돌리지 않은 프로브는 `null`** 로 간다. 그러면 판정이 그 단계를 달성으로
55
55
  * 만들지 않고 `unmeasured` 로 말한다 — 스위트를 절반만 돌리고 초록을 주장할 길이 없다.
56
56
  */
57
57
  function foldProbeResults(results) {
@@ -1 +1 @@
1
- {"version":3,"file":"integration-probes.js","sourceRoot":"","sources":["../../server/engine/integration-probes.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 연결 적합성 하네스 — **통합을 시험 스위트처럼 돌린다.**\n *\n * 선언 적합성 하네스 세 벌과 같은 규율을 **연결**에 적용한다: 사실이 조용히 사라지는 것을 막는다.\n * 다른 점은 대상이 우리 선언이 아니라 **남의 시스템**이라는 것이고, 그래서 두 규칙이 더 붙는다.\n *\n * ① **돌리지 않은 프로브는 「실패」가 아니라 「모른다」다.** 실패로 접으면 사람이 그 빨간 칸을\n * 없애려고 무엇이든 하게 되고, 모른다로 접으면 **무엇을 아직 모르는지**가 남는다. 판정\n * (`assessStagePath`)이 「모른다」를 달성으로 바꾸지 않으므로 이 구분이 끝까지 살아 있다.\n * ② **쓰기 프로브는 명시 동의 없이 돌지 않는다.** 이 스위트는 고객의 운영 시스템을 두드린다.\n * 읽기는 기본으로 돌리고, 남의 공장에 명령을 넣는 프로브는 사람이 켜야 돈다 — 켜지 않으면\n * 폐루프 칸은 「검증되지 않음」으로 남는다(그게 사실이다).\n *\n * 순수 모듈이다(전송·DB 무의존). 실행기는 이 선언을 읽어 프로브를 돌리고 `ProbeResult[]` 를 낸다.\n * 화면은 그 결과와 `assessStagePath` 의 판정을 함께 보인다 — 초록/빨강과 **그 초록이 무엇을 증명하나**.\n */\n\nimport type { StageEvidence, StageId } from './stage-path.ts'\n\nexport type ProbeId =\n | 'connect' // 붙는다\n | 'discover' // 사이트가 열거된다\n | 'master' // 구조가 들어온다\n | 'master-warnings' // 못 옮긴 값을 세었다\n | 'items' // 자리에 물품이 놓인다(비직렬 수량 포함)\n | 'live-ingest' // 이벤트가 들어온다\n | 'live-removal' // **반출이 관측된다** — 진짜 delta 의 증거\n | 'declarations' // 공정 선언이 실려 있다\n | 'forecast' // 예측이 돈다\n | 'actuate-declared' // 액추에이션 능력이 선언∩구현이다\n | 'actuate-effect' // 명령의 효과가 상관되어 돌아온다 (쓰기)\n\nexport interface ProbeSpec {\n id: ProbeId\n /** 이 프로브가 증거를 대는 단계. */\n stage: StageId\n /** **무엇을 증명하나** — 언어중립 키(사람 말은 표현계층이 렌더). 「무엇을 호출하나」가 아니다. */\n proves: string\n /** 대상 시스템을 바꾸는가. 참이면 사람이 켜야 돈다. */\n writes: boolean\n /** 이 프로브가 실패하면 뒤 프로브를 돌릴 의미가 없다. */\n gating: boolean\n}\n\n/**\n * 시퀀스 — 순서가 곧 방법론이다. 앞이 막히면 뒤는 돌리지 않는다(돌려도 답이 무의미하다).\n *\n * `master-warnings` 를 **별개 프로브**로 둔 이유: 구조가 들어오는 것과 못 옮긴 값을 세는 것은 다른\n * 사실이고, 세지 않은 인제스트는 도크를 랙으로 바꿔 놓고도 성공으로 보인다.\n */\nexport const PROBE_SEQUENCE: readonly ProbeSpec[] = [\n { id: 'connect', stage: 'structure', proves: 'probe.connect', writes: false, gating: true },\n { id: 'discover', stage: 'structure', proves: 'probe.discover', writes: false, gating: true },\n { id: 'master', stage: 'structure', proves: 'probe.master', writes: false, gating: true },\n { id: 'master-warnings', stage: 'structure', proves: 'probe.masterWarnings', writes: false, gating: false },\n { id: 'items', stage: 'structure', proves: 'probe.items', writes: false, gating: false },\n { id: 'live-ingest', stage: 'live', proves: 'probe.liveIngest', writes: false, gating: true },\n { id: 'live-removal', stage: 'live', proves: 'probe.liveRemoval', writes: false, gating: false },\n { id: 'declarations', stage: 'forecast', proves: 'probe.declarations', writes: false, gating: false },\n { id: 'forecast', stage: 'forecast', proves: 'probe.forecast', writes: false, gating: false },\n { id: 'actuate-declared', stage: 'closed-loop', proves: 'probe.actuateDeclared', writes: false, gating: true },\n { id: 'actuate-effect', stage: 'closed-loop', proves: 'probe.actuateEffect', writes: true, gating: false }\n]\n\n/** 프로브 한 건의 결과. `ran: false` = 돌리지 않았다(실패가 아니다). */\nexport interface ProbeResult {\n id: ProbeId\n ran: boolean\n /**\n * 이 대상에 **해당하지 않는다**. 통과도 실패도 미측정도 아닌 네 번째 값이다.\n *\n * 시뮬레이션 트윈에 라이브 프로브를 들이대면 빨간 칸이 생기는데 그건 결함이 아니다 — 시뮬은 원천에서\n * 받는 것이 없다. 레퍼런스 없이 템플릿으로 세운 트윈에는 옮겨 온 마스터가 없으므로 「못 옮긴 값」도 없다.\n * 이걸 통과로 접으면 없는 능력을 주장하고, 미측정으로 접으면 영원히 배선을 기다린다.\n */\n applicable?: boolean\n /** 돌렸을 때의 판정. 돌리지 않았으면 무의미하다. */\n passed?: boolean\n /** 센 값(자리 수·반출 건수·선언 수 등). 세지 않았으면 없다. */\n count?: number\n /** 선언 적재량 프로브만 — `declared`/`total`. */\n coverage?: { declared: number; total: number }\n /** 왜 실패했나·왜 건너뛰었나. 언어중립 코드 또는 원문 오류. */\n detail?: string\n}\n\nconst by = (rs: readonly ProbeResult[], id: ProbeId) => rs.find(r => r.id === id)\n/** 돌렸고 셌으면 그 수, 아니면 `null`(모른다). 실패한 프로브의 수를 0 으로 읽지 않는다. */\n/**\n * 센 값은 **판정과 무관한 사실**이다 — `passed` 를 요구하면 「세어서 0 을 얻었다」가 「모른다」로 접힌다.\n * 그 구분이 이 하네스의 전부이므로 여기서 잃으면 나머지가 무의미하다. 해당 없음만 `null` 이다.\n */\nconst counted = (r: ProbeResult | undefined): number | null =>\n r?.applicable === false ? null : r?.ran && r.count !== undefined ? r.count : null\n/** 돌렸고 셌으면 그 수, 아니면 0. **존재 여부**를 묻는 자리에만 쓴다(모름과 0 을 구별할 필요가 없는 축). */\nconst countedOrZero = (r: ProbeResult | undefined): number => counted(r) ?? 0\n\n/**\n * 프로브 결과를 **증거**로 접는다.\n *\n * 접는 규칙의 정수: **돌리지 않은 프로브는 `null`** 로 간다. 그러면 판정이 그 단계를 달성으로\n * 만들지 않고 `unmeasured` 로 말한다 — 스위트를 절반만 돌리고 초록을 주장할 길이 없다.\n */\nexport function foldProbeResults(results: readonly ProbeResult[]): StageEvidence {\n return {\n locationCount: countedOrZero(by(results, 'master')),\n itemCount: countedOrZero(by(results, 'items')),\n masterWarnings: counted(by(results, 'master-warnings')),\n /* 해당 없음은 **미측정이 아니다** — 물을 대상이 아닌 축을 판정에서 뺀다. */\n masterWarningsApplicable: by(results, 'master-warnings')?.applicable !== false,\n liveIngestTotal: countedOrZero(by(results, 'live-ingest')),\n removalsObserved: counted(by(results, 'live-removal')),\n forecastRan: by(results, 'forecast')?.ran === true && by(results, 'forecast')?.passed === true,\n specCoverage: (() => {\n const r = by(results, 'declarations')\n return r?.ran && r.passed && r.coverage ? r.coverage : null\n })(),\n actuateCapability: by(results, 'actuate-declared')?.passed === true,\n correlatedEffectObserved: counted(by(results, 'actuate-effect'))\n }\n}\n\n/**\n * 지금 돌릴 프로브 — 게이팅과 쓰기 동의를 지킨다.\n *\n * 앞의 게이팅 프로브가 실패했으면 **거기서 멈춘다.** 뒤를 돌려 빨간 칸을 늘리는 것은 정보를 주지\n * 않는다(원인은 이미 앞에 있다). 쓰기 프로브는 `allowWrites` 없이는 목록에 들어오지 않는다.\n */\nexport function plannedProbes(results: readonly ProbeResult[], opts: { allowWrites?: boolean } = {}): ProbeId[] {\n const out: ProbeId[] = []\n for (const spec of PROBE_SEQUENCE) {\n if (spec.writes && !opts.allowWrites) continue\n const prior = by(results, spec.id)\n if (prior?.applicable === false) continue // 해당 없는 프로브는 계획하지 않는다\n if (!prior?.ran) out.push(spec.id)\n if (spec.gating && prior?.ran && prior.passed === false) break\n }\n return out\n}\n\n/** 해당 없음으로 판정된 프로브 — 화면이 「왜 회색인가」를 말할 수 있게 사유와 함께 낸다. */\nexport function notApplicable(results: readonly ProbeResult[]): { id: ProbeId; detail?: string }[] {\n return results.filter(r => r.applicable === false).map(r => ({ id: r.id, ...(r.detail ? { detail: r.detail } : {}) }))\n}\n"]}
1
+ {"version":3,"file":"integration-probes.js","sourceRoot":"","sources":["../../server/engine/integration-probes.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 연결 적합성 하네스 — **통합을 시험 스위트처럼 돌린다.**\n *\n * 선언 적합성 하네스 세 벌과 같은 규율을 **연결**에 적용한다: 사실이 오류 없이 사라지는 것을 막는다.\n * 다른 점은 대상이 우리 선언이 아니라 **남의 시스템**이라는 것이고, 그래서 두 규칙이 더 붙는다.\n *\n * ① **돌리지 않은 프로브는 「실패」가 아니라 「모른다」다.** 실패로 처리하면 사람이 그 빨간 칸을\n * 없애려고 무엇이든 하게 되고, 모른다로 처리하면 **무엇을 아직 모르는지**가 남는다. 판정\n * (`assessStagePath`)이 「모른다」를 달성으로 바꾸지 않으므로 이 구분이 끝까지 살아 있다.\n * ② **쓰기 프로브는 명시 동의 없이 돌지 않는다.** 이 스위트는 고객의 운영 시스템을 두드린다.\n * 읽기는 기본으로 돌리고, 남의 공장에 명령을 넣는 프로브는 사람이 켜야 돈다 — 켜지 않으면\n * 폐루프 칸은 「검증되지 않음」으로 남는다(그게 사실이다).\n *\n * 순수 모듈이다(전송·DB 무의존). 실행기는 이 선언을 읽어 프로브를 돌리고 `ProbeResult[]` 를 낸다.\n * 화면은 그 결과와 `assessStagePath` 의 판정을 함께 보인다 — 초록/빨강과 **그 초록이 무엇을 증명하나**.\n */\n\nimport type { StageEvidence, StageId } from './stage-path.ts'\n\nexport type ProbeId =\n | 'connect' // 붙는다\n | 'discover' // 사이트가 열거된다\n | 'master' // 구조가 들어온다\n | 'master-warnings' // 못 옮긴 값을 세었다\n | 'items' // 자리에 물품이 놓인다(비직렬 수량 포함)\n | 'live-ingest' // 이벤트가 들어온다\n | 'live-removal' // **반출이 관측된다** — 진짜 delta 의 증거\n | 'declarations' // 공정 선언이 실려 있다\n | 'forecast' // 예측이 돈다\n | 'actuate-declared' // 액추에이션 능력이 선언∩구현이다\n | 'actuate-effect' // 명령의 효과가 상관되어 돌아온다 (쓰기)\n\nexport interface ProbeSpec {\n id: ProbeId\n /** 이 프로브가 증거를 대는 단계. */\n stage: StageId\n /** **무엇을 증명하나** — 언어중립 키(사람 말은 표현계층이 렌더). 「무엇을 호출하나」가 아니다. */\n proves: string\n /** 대상 시스템을 바꾸는가. 참이면 사람이 켜야 돈다. */\n writes: boolean\n /** 이 프로브가 실패하면 뒤 프로브를 돌릴 의미가 없다. */\n gating: boolean\n}\n\n/**\n * 시퀀스 — 순서가 곧 방법론이다. 앞이 막히면 뒤는 돌리지 않는다(돌려도 답이 무의미하다).\n *\n * `master-warnings` 를 **별개 프로브**로 둔 이유: 구조가 들어오는 것과 못 옮긴 값을 세는 것은 다른\n * 사실이고, 세지 않은 인제스트는 도크를 랙으로 바꿔 놓고도 성공으로 보인다.\n */\nexport const PROBE_SEQUENCE: readonly ProbeSpec[] = [\n { id: 'connect', stage: 'structure', proves: 'probe.connect', writes: false, gating: true },\n { id: 'discover', stage: 'structure', proves: 'probe.discover', writes: false, gating: true },\n { id: 'master', stage: 'structure', proves: 'probe.master', writes: false, gating: true },\n { id: 'master-warnings', stage: 'structure', proves: 'probe.masterWarnings', writes: false, gating: false },\n { id: 'items', stage: 'structure', proves: 'probe.items', writes: false, gating: false },\n { id: 'live-ingest', stage: 'live', proves: 'probe.liveIngest', writes: false, gating: true },\n { id: 'live-removal', stage: 'live', proves: 'probe.liveRemoval', writes: false, gating: false },\n { id: 'declarations', stage: 'forecast', proves: 'probe.declarations', writes: false, gating: false },\n { id: 'forecast', stage: 'forecast', proves: 'probe.forecast', writes: false, gating: false },\n { id: 'actuate-declared', stage: 'closed-loop', proves: 'probe.actuateDeclared', writes: false, gating: true },\n { id: 'actuate-effect', stage: 'closed-loop', proves: 'probe.actuateEffect', writes: true, gating: false }\n]\n\n/** 프로브 한 건의 결과. `ran: false` = 돌리지 않았다(실패가 아니다). */\nexport interface ProbeResult {\n id: ProbeId\n ran: boolean\n /**\n * 이 대상에 **해당하지 않는다**. 통과도 실패도 미측정도 아닌 네 번째 값이다.\n *\n * 시뮬레이션 트윈에 라이브 프로브를 들이대면 빨간 칸이 생기는데 그건 결함이 아니다 — 시뮬은 원천에서\n * 받는 것이 없다. 레퍼런스 없이 템플릿으로 세운 트윈에는 옮겨 온 마스터가 없으므로 「못 옮긴 값」도 없다.\n * 이걸 통과로 처리하면 없는 능력을 주장하고, 미측정으로 처리하면 영원히 배선을 기다린다.\n */\n applicable?: boolean\n /** 돌렸을 때의 판정. 돌리지 않았으면 무의미하다. */\n passed?: boolean\n /** 센 값(자리 수·반출 건수·선언 수 등). 세지 않았으면 없다. */\n count?: number\n /** 선언 적재량 프로브만 — `declared`/`total`. */\n coverage?: { declared: number; total: number }\n /** 왜 실패했나·왜 건너뛰었나. 언어중립 코드 또는 원문 오류. */\n detail?: string\n}\n\nconst by = (rs: readonly ProbeResult[], id: ProbeId) => rs.find(r => r.id === id)\n/** 돌렸고 셌으면 그 수, 아니면 `null`(모른다). 실패한 프로브의 수를 0 으로 읽지 않는다. */\n/**\n * 센 값은 **판정과 무관한 사실**이다 — `passed` 를 요구하면 「세어서 0 을 얻었다」가 「모른다」와 구별되지 않는다.\n * 그 구분이 이 하네스의 전부이므로 여기서 잃으면 나머지가 무의미하다. 해당 없음만 `null` 이다.\n */\nconst counted = (r: ProbeResult | undefined): number | null =>\n r?.applicable === false ? null : r?.ran && r.count !== undefined ? r.count : null\n/** 돌렸고 셌으면 그 수, 아니면 0. **존재 여부**를 묻는 자리에만 쓴다(모름과 0 을 구별할 필요가 없는 축). */\nconst countedOrZero = (r: ProbeResult | undefined): number => counted(r) ?? 0\n\n/**\n * 프로브 결과를 **증거**로 처리한다.\n *\n * 계산 규칙의 정수: **돌리지 않은 프로브는 `null`** 로 간다. 그러면 판정이 그 단계를 달성으로\n * 만들지 않고 `unmeasured` 로 말한다 — 스위트를 절반만 돌리고 초록을 주장할 길이 없다.\n */\nexport function foldProbeResults(results: readonly ProbeResult[]): StageEvidence {\n return {\n locationCount: countedOrZero(by(results, 'master')),\n itemCount: countedOrZero(by(results, 'items')),\n masterWarnings: counted(by(results, 'master-warnings')),\n /* 해당 없음은 **미측정이 아니다** — 물을 대상이 아닌 축을 판정에서 뺀다. */\n masterWarningsApplicable: by(results, 'master-warnings')?.applicable !== false,\n liveIngestTotal: countedOrZero(by(results, 'live-ingest')),\n removalsObserved: counted(by(results, 'live-removal')),\n forecastRan: by(results, 'forecast')?.ran === true && by(results, 'forecast')?.passed === true,\n specCoverage: (() => {\n const r = by(results, 'declarations')\n return r?.ran && r.passed && r.coverage ? r.coverage : null\n })(),\n actuateCapability: by(results, 'actuate-declared')?.passed === true,\n correlatedEffectObserved: counted(by(results, 'actuate-effect'))\n }\n}\n\n/**\n * 지금 돌릴 프로브 — 게이팅과 쓰기 동의를 지킨다.\n *\n * 앞의 게이팅 프로브가 실패했으면 **거기서 멈춘다.** 뒤를 돌려 빨간 칸을 늘리는 것은 정보를 주지\n * 않는다(원인은 이미 앞에 있다). 쓰기 프로브는 `allowWrites` 없이는 목록에 들어오지 않는다.\n */\nexport function plannedProbes(results: readonly ProbeResult[], opts: { allowWrites?: boolean } = {}): ProbeId[] {\n const out: ProbeId[] = []\n for (const spec of PROBE_SEQUENCE) {\n if (spec.writes && !opts.allowWrites) continue\n const prior = by(results, spec.id)\n if (prior?.applicable === false) continue // 해당 없는 프로브는 계획하지 않는다\n if (!prior?.ran) out.push(spec.id)\n if (spec.gating && prior?.ran && prior.passed === false) break\n }\n return out\n}\n\n/** 해당 없음으로 판정된 프로브 — 화면이 「왜 회색인가」를 말할 수 있게 사유와 함께 낸다. */\nexport function notApplicable(results: readonly ProbeResult[]): { id: ProbeId; detail?: string }[] {\n return results.filter(r => r.applicable === false).map(r => ({ id: r.id, ...(r.detail ? { detail: r.detail } : {}) }))\n}\n"]}
@@ -7,7 +7,7 @@
7
7
  * "어제보다 나아졌다." **어제도 나빴으면 그 말은 쓸모가 없다.** 목표(`kpi-target`)가 그 한계를 한쪽에서
8
8
  * 풀지만, 목표를 아직 걸지 않은 현장은 여전히 자기 숫자가 평소와 같은지도 알 수 없다.
9
9
  *
10
- * 최근 며칠의 **같은 시간대**를 함께 접으면 목표 없이도 말할 수 있다 — "오늘 이 시간대는 지난 7일 중
10
+ * 최근 며칠의 **같은 시간대**를 함께 계산하면 목표 없이도 말할 수 있다 — "오늘 이 시간대는 지난 7일 중
11
11
  * 가장 낮다." 그것이 이 파일이 만드는 것이다: 창 여러 개의 **분포**와 그 안에서 현재의 **자리**.
12
12
  *
13
13
  * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────
@@ -19,12 +19,12 @@
19
19
  * 퍼센트 임계를 두면 그 숫자가 곧 숨은 목표가 된다.
20
20
  *
21
21
  * ── 정직성 규율 (이 도메인의 핵심) ──────────────────────────────────────────
22
- * ① **결측 창을 조용히 빼지 않는다.** 요청한 창 전부를 `measured` 와 함께 돌려주고, 분포는 측정된
22
+ * ① **결측 창을 알리지 않고 빼지 않는다.** 요청한 창 전부를 `measured` 와 함께 돌려주고, 분포는 측정된
23
23
  * 것만으로 만든다. 빼고 말하면 "지난 7일" 이 실제로는 3일이었다는 것을 아무도 모른다.
24
24
  * ② **표본이 둘 미만이면 자리를 말하지 않는다.** 하나를 분포라 부를 수 없다.
25
25
  * ③ **현재 값이 없으면 자리를 말하지 않는다.** 결측을 0 으로 읽으면 시간 지표는 "낮을수록 좋다" 라서
26
26
  * **일이 없던 구간이 최고 성적**으로 보고된다(목표 판정에서 이미 겪은 결함이다).
27
- * ④ 왜 말하지 않는지를 `reason` 으로 밝힌다 — 조용히 빈 값을 주면 화면이 "분포 없음" 을 "평범함" 으로
27
+ * ④ 왜 말하지 않는지를 `reason` 으로 밝힌다 — 알리지 않고 빈 값을 주면 화면이 "분포 없음" 을 "평범함" 으로
28
28
  * 그린다.
29
29
  */
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -90,7 +90,7 @@ function summarizeBaseline(specs, current, inputs) {
90
90
  measuredCount: values.length,
91
91
  ...(sorted.length ? { min: sorted[0], median: medianOf(sorted), max: sorted[sorted.length - 1] } : {})
92
92
  };
93
- /* 자리를 말할 수 없는 두 경우 — 이유를 밝힌다(조용한 빈 값이 "평범함" 으로 읽힌다). */
93
+ /* 자리를 말할 수 없는 두 경우 — 이유를 밝힌다(들어온 것이 없는 빈 값이 "평범함" 으로 읽힌다). */
94
94
  if (!hasCurrent)
95
95
  row.noPosition = 'no-current';
96
96
  else if (sorted.length < 2)
@@ -1 +1 @@
1
- {"version":3,"file":"kpi-baseline.js","sourceRoot":"","sources":["../../server/engine/kpi-baseline.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 여러 구간 동시 비교 — **\"어제보다 나아졌다\" 를 넘어선다.** 순수.\n *\n * ── 왜 필요한가 (성과 맵 ③) ─────────────────────────────────────────────────\n * 지금 비교 축은 **한 구간**만 본다(직전 구간 또는 하루 전). 그래서 판단이 딱 한 문장에서 멈춘다:\n * \"어제보다 나아졌다.\" **어제도 나빴으면 그 말은 쓸모가 없다.** 목표(`kpi-target`)가 그 한계를 한쪽에서\n * 풀지만, 목표를 아직 걸지 않은 현장은 여전히 자기 숫자가 평소와 같은지도 알 수 없다.\n *\n * 최근 며칠의 **같은 시간대**를 함께 접으면 목표 없이도 말할 수 있다 — \"오늘 이 시간대는 지난 7일 중\n * 가장 낮다.\" 그것이 이 파일이 만드는 것이다: 창 여러 개의 **분포**와 그 안에서 현재의 **자리**.\n *\n * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────\n * · **지표 어휘를 모른다.** 이름·방향·값 꺼내는 법은 `kpi-target.ts` 의 `TWIN_METRIC` 이 소유한다. 여기는\n * **이미 꺼낸 값**만 받는다 — 어휘가 두 곳에 생기면 그 사이에 매핑 표가 자라고 그것이 곧 방언이다.\n * (덤으로: 이 모듈은 런타임 import 가 없어 테스트가 소스를 그대로 든다.)\n * · **DB·조회를 모른다.** 창 목록만 계산하고, 그 창을 읽는 것은 호출부의 일이다.\n * · **좋고 나쁨의 임계를 발명하지 않는다.** \"평소보다 나쁘다\" 는 중앙값과의 비교로만 말한다 — 임의의\n * 퍼센트 임계를 두면 그 숫자가 곧 숨은 목표가 된다.\n *\n * ── 정직성 규율 (이 도메인의 핵심) ──────────────────────────────────────────\n * ① **결측 창을 조용히 빼지 않는다.** 요청한 창 전부를 `measured` 와 함께 돌려주고, 분포는 측정된\n * 것만으로 만든다. 빼고 말하면 \"지난 7일\" 이 실제로는 3일이었다는 것을 아무도 모른다.\n * ② **표본이 둘 미만이면 자리를 말하지 않는다.** 하나를 분포라 부를 수 없다.\n * ③ **현재 값이 없으면 자리를 말하지 않는다.** 결측을 0 으로 읽으면 시간 지표는 \"낮을수록 좋다\" 라서\n * **일이 없던 구간이 최고 성적**으로 보고된다(목표 판정에서 이미 겪은 결함이다).\n * ④ 왜 말하지 않는지를 `reason` 으로 밝힌다 — 조용히 빈 값을 주면 화면이 \"분포 없음\" 을 \"평범함\" 으로\n * 그린다.\n */\n\n/** 창 하나 — 폴드의 창과 같은 모양(ms). */\nexport interface BaselineWindow {\n fromMs: number\n toMs: number\n}\n\n/** 며칠 전 같은 시간대인가, 몇 주 전 같은 요일·시간대인가. */\nexport type BaselineStride = 'day' | 'week'\n\n/** 한 번에 볼 수 있는 과거 구간 수 — 창마다 조회가 한 번 더 붙으므로 상한을 둔다. */\nexport const MAX_BASELINE_PERIODS = 14\n\nconst MS_DAY = 24 * 60 * 60_000\n\n/**\n * 비교할 과거 창들 — 현재 창을 **같은 길이로** 하루(또는 한 주)씩 뒤로 옮긴다.\n *\n * 가까운 것부터 준다(어제 → 그제 → …). 화면이 시간 순서를 다시 정하지 않아도 되게.\n *\n * **한계를 밝힌다**: 옮기는 것은 ms 산술이다. 트윈 공간은 고정 오프셋(`utcOffsetMinutes`)을 선언하므로\n * 현재 모델에서는 \"같은 시간대\" 가 정확하다. 일광절약시간을 쓰는 시간대를 나중에 지원하면 전환이 있는\n * 주의 하루가 한 시간 밀린다 — 그때는 오프셋 기준 캘린더 산술로 바꿔야 한다(지금 지어내지 않는다).\n */\nexport function baselineWindows(\n fromMs: number,\n toMs: number,\n periods: number,\n stride: BaselineStride = 'day'\n): BaselineWindow[] {\n if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs <= fromMs) return []\n const count = Math.max(0, Math.min(MAX_BASELINE_PERIODS, Math.floor(periods)))\n const step = stride === 'week' ? 7 * MS_DAY : MS_DAY\n\n return Array.from({ length: count }, (_, i) => ({\n fromMs: fromMs - step * (i + 1),\n toMs: toMs - step * (i + 1)\n }))\n}\n\n/** 지표의 성질 — 이름과 값 꺼내기는 호출부(`TWIN_METRIC`)가 소유한다. */\nexport interface MetricSpecLike {\n direction: 'higher' | 'lower'\n unit: string\n}\n\n/** 과거 창 하나에서 꺼낸 값들 — 그 창에 기록이 없으면 값이 `undefined` 다. */\nexport interface BaselineSampleInput<M extends string> {\n fromMs: number\n toMs: number\n values: Partial<Record<M, number | undefined>>\n}\n\n/** 과거 창 하나의 결과 — **결측도 남긴다**(빼면 \"지난 7일\" 이 거짓이 된다). */\nexport interface BaselineSample {\n fromMs: number\n toMs: number\n measured: boolean\n value?: number\n}\n\n/** 왜 자리를 말하지 않는지. */\nexport type BaselineNoPosition = 'no-current' | 'too-few-samples'\n\n/**\n * 분포 안에서 현재의 자리 — **방향을 적용한 읽기.**\n *\n * 임의의 임계가 없다: 표본 전부보다 좋으면 `best`, 전부보다 나쁘면 `worst`, 그 사이는 중앙값을 기준으로\n * 구분한다. `typical` 은 중앙값과 **같을 때**만이다.\n */\nexport type BaselineStanding = 'best' | 'better-than-typical' | 'typical' | 'worse-than-typical' | 'worst'\n\nexport interface MetricBaseline<M extends string = string> {\n metric: M\n direction: 'higher' | 'lower'\n unit: string\n /** 지금 창의 값 — 없으면 `undefined`(0 이 아니다). */\n current?: number\n /** 요청한 과거 창 전부(결측 포함, 가까운 것부터). */\n samples: BaselineSample[]\n /** 요청한 창 수 / 그중 실제로 측정된 수 — 둘을 함께 봐야 \"지난 7일\" 을 믿을 수 있다. */\n requested: number\n measuredCount: number\n /** 측정된 표본만의 요약. 표본이 없으면 전부 `undefined`. */\n min?: number\n median?: number\n max?: number\n /** 측정 표본을 오름차순으로 놓았을 때 현재 값이 몇 번째인가(1 = 가장 작음) — **방향 없는 사실**. */\n rank?: number\n /** 그 순위를 셀 때의 표본 수(+현재 1). 순위만 보면 표본이 둘이었는지 열이었는지 알 수 없다. */\n rankOf?: number\n /** 방향을 적용한 읽기. 말할 수 없으면 없다. */\n standing?: BaselineStanding\n /** 자리를 말하지 않은 이유. */\n noPosition?: BaselineNoPosition\n}\n\n/** 중앙값 — 짝수 개면 가운데 둘의 평균(폴드의 분위수 규약과 달리 여기 표본은 아주 작다). */\nfunction medianOf(sorted: readonly number[]): number | undefined {\n if (!sorted.length) return undefined\n const mid = sorted.length >> 1\n return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2\n}\n\n/**\n * 지표별로 분포와 자리를 만든다.\n *\n * @param specs 지표 성질(방향·단위). 열거된 지표만 낸다 — 모르는 이름을 짐작하지 않는다.\n * @param current 지금 창에서 꺼낸 값들(결측은 `undefined`).\n * @param inputs 과거 창들에서 꺼낸 값들(가까운 것부터).\n */\nexport function summarizeBaseline<M extends string>(\n specs: Readonly<Record<M, MetricSpecLike>>,\n current: Partial<Record<M, number | undefined>>,\n inputs: readonly BaselineSampleInput<M>[]\n): MetricBaseline<M>[] {\n const out: MetricBaseline<M>[] = []\n\n for (const metric of Object.keys(specs) as M[]) {\n const spec = specs[metric]\n const samples: BaselineSample[] = inputs.map(s => {\n const v = s.values[metric]\n const measured = typeof v === 'number' && Number.isFinite(v)\n return measured ? { fromMs: s.fromMs, toMs: s.toMs, measured: true, value: v as number } : { fromMs: s.fromMs, toMs: s.toMs, measured: false }\n })\n\n const values = samples.filter(s => s.measured).map(s => s.value as number)\n const sorted = [...values].sort((a, b) => a - b)\n const cur = current[metric]\n const hasCurrent = typeof cur === 'number' && Number.isFinite(cur)\n\n const row: MetricBaseline<M> = {\n metric,\n direction: spec.direction,\n unit: spec.unit,\n ...(hasCurrent ? { current: cur as number } : {}),\n samples,\n requested: inputs.length,\n measuredCount: values.length,\n ...(sorted.length ? { min: sorted[0], median: medianOf(sorted), max: sorted[sorted.length - 1] } : {})\n }\n\n /* 자리를 말할 수 없는 두 경우 — 이유를 밝힌다(조용한 빈 값이 \"평범함\" 으로 읽힌다). */\n if (!hasCurrent) row.noPosition = 'no-current'\n else if (sorted.length < 2) row.noPosition = 'too-few-samples'\n else {\n const value = cur as number\n const median = medianOf(sorted) as number\n /* 오름차순에서 몇 번째인가 — 같은 값은 뒤에 놓는다(동률이면 \"더 큰 쪽\" 으로 세지 않는다). */\n row.rank = sorted.filter(v => v < value).length + 1\n row.rankOf = sorted.length + 1\n\n const higherIsBetter = spec.direction === 'higher'\n const beatsAll = higherIsBetter ? value > sorted[sorted.length - 1] : value < sorted[0]\n const losesToAll = higherIsBetter ? value < sorted[0] : value > sorted[sorted.length - 1]\n /* 중앙값보다 큰 것이 \"좋은\" 쪽인가 — 방향을 여기 한 번만 적용한다. */\n const aboveMedianIsBetter = higherIsBetter === value > median\n\n row.standing = beatsAll\n ? 'best'\n : losesToAll\n ? 'worst'\n : value === median\n ? 'typical'\n : aboveMedianIsBetter\n ? 'better-than-typical'\n : 'worse-than-typical'\n }\n\n out.push(row)\n }\n\n return out\n}\n"]}
1
+ {"version":3,"file":"kpi-baseline.js","sourceRoot":"","sources":["../../server/engine/kpi-baseline.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;;;AAyBH,0CAcC;AAyED,8CA8DC;AAnKD,uDAAuD;AAC1C,QAAA,oBAAoB,GAAG,EAAE,CAAA;AAEtC,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,CAAA;AAE/B;;;;;;;;GAQG;AACH,SAAgB,eAAe,CAC7B,MAAc,EACd,IAAY,EACZ,OAAe,EACf,SAAyB,KAAK;IAE9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,MAAM;QAAE,OAAO,EAAE,CAAA;IACnF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,4BAAoB,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IAC9E,MAAM,IAAI,GAAG,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,CAAA;IAEpD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9C,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;KAC5B,CAAC,CAAC,CAAA;AACL,CAAC;AA2DD,0DAA0D;AAC1D,SAAS,QAAQ,CAAC,MAAyB;IACzC,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IACpC,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAA;IAC9B,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAC/B,KAA0C,EAC1C,OAA+C,EAC/C,MAAyC;IAEzC,MAAM,GAAG,GAAwB,EAAE,CAAA;IAEnC,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAQ,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAA;QAC1B,MAAM,OAAO,GAAqB,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;YAC/C,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAC1B,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAA;YAC5D,OAAO,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,CAAW,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;QAChJ,CAAC,CAAC,CAAA;QAEF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAe,CAAC,CAAA;QAC1E,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QAChD,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;QAC3B,MAAM,UAAU,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;QAElE,MAAM,GAAG,GAAsB;YAC7B,MAAM;YACN,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,OAAO;YACP,SAAS,EAAE,MAAM,CAAC,MAAM;YACxB,aAAa,EAAE,MAAM,CAAC,MAAM;YAC5B,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvG,CAAA;QAED,8DAA8D;QAC9D,IAAI,CAAC,UAAU;YAAE,GAAG,CAAC,UAAU,GAAG,YAAY,CAAA;aACzC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,UAAU,GAAG,iBAAiB,CAAA;aACzD,CAAC;YACJ,MAAM,KAAK,GAAG,GAAa,CAAA;YAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAW,CAAA;YACzC,2DAA2D;YAC3D,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;YACnD,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;YAE9B,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAA;YAClD,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAA;YACvF,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;YACzF,6CAA6C;YAC7C,MAAM,mBAAmB,GAAG,cAAc,KAAK,KAAK,GAAG,MAAM,CAAA;YAE7D,GAAG,CAAC,QAAQ,GAAG,QAAQ;gBACrB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,UAAU;oBACV,CAAC,CAAC,OAAO;oBACT,CAAC,CAAC,KAAK,KAAK,MAAM;wBAChB,CAAC,CAAC,SAAS;wBACX,CAAC,CAAC,mBAAmB;4BACnB,CAAC,CAAC,qBAAqB;4BACvB,CAAC,CAAC,oBAAoB,CAAA;QAChC,CAAC;QAED,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACf,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/*\n * 여러 구간 동시 비교 — **\"어제보다 나아졌다\" 를 넘어선다.** 순수.\n *\n * ── 왜 필요한가 (성과 맵 ③) ─────────────────────────────────────────────────\n * 지금 비교 축은 **한 구간**만 본다(직전 구간 또는 하루 전). 그래서 판단이 딱 한 문장에서 멈춘다:\n * \"어제보다 나아졌다.\" **어제도 나빴으면 그 말은 쓸모가 없다.** 목표(`kpi-target`)가 그 한계를 한쪽에서\n * 풀지만, 목표를 아직 걸지 않은 현장은 여전히 자기 숫자가 평소와 같은지도 알 수 없다.\n *\n * 최근 며칠의 **같은 시간대**를 함께 계산하면 목표 없이도 말할 수 있다 — \"오늘 이 시간대는 지난 7일 중\n * 가장 낮다.\" 그것이 이 파일이 만드는 것이다: 창 여러 개의 **분포**와 그 안에서 현재의 **자리**.\n *\n * ── 이 파일이 하지 않는 것 ──────────────────────────────────────────────────\n * · **지표 어휘를 모른다.** 이름·방향·값 꺼내는 법은 `kpi-target.ts` 의 `TWIN_METRIC` 이 소유한다. 여기는\n * **이미 꺼낸 값**만 받는다 — 어휘가 두 곳에 생기면 그 사이에 매핑 표가 자라고 그것이 곧 방언이다.\n * (덤으로: 이 모듈은 런타임 import 가 없어 테스트가 소스를 그대로 든다.)\n * · **DB·조회를 모른다.** 창 목록만 계산하고, 그 창을 읽는 것은 호출부의 일이다.\n * · **좋고 나쁨의 임계를 발명하지 않는다.** \"평소보다 나쁘다\" 는 중앙값과의 비교로만 말한다 — 임의의\n * 퍼센트 임계를 두면 그 숫자가 곧 숨은 목표가 된다.\n *\n * ── 정직성 규율 (이 도메인의 핵심) ──────────────────────────────────────────\n * ① **결측 창을 알리지 않고 빼지 않는다.** 요청한 창 전부를 `measured` 와 함께 돌려주고, 분포는 측정된\n * 것만으로 만든다. 빼고 말하면 \"지난 7일\" 이 실제로는 3일이었다는 것을 아무도 모른다.\n * ② **표본이 둘 미만이면 자리를 말하지 않는다.** 하나를 분포라 부를 수 없다.\n * ③ **현재 값이 없으면 자리를 말하지 않는다.** 결측을 0 으로 읽으면 시간 지표는 \"낮을수록 좋다\" 라서\n * **일이 없던 구간이 최고 성적**으로 보고된다(목표 판정에서 이미 겪은 결함이다).\n * ④ 왜 말하지 않는지를 `reason` 으로 밝힌다 — 알리지 않고 빈 값을 주면 화면이 \"분포 없음\" 을 \"평범함\" 으로\n * 그린다.\n */\n\n/** 창 하나 — 폴드의 창과 같은 모양(ms). */\nexport interface BaselineWindow {\n fromMs: number\n toMs: number\n}\n\n/** 며칠 전 같은 시간대인가, 몇 주 전 같은 요일·시간대인가. */\nexport type BaselineStride = 'day' | 'week'\n\n/** 한 번에 볼 수 있는 과거 구간 수 — 창마다 조회가 한 번 더 붙으므로 상한을 둔다. */\nexport const MAX_BASELINE_PERIODS = 14\n\nconst MS_DAY = 24 * 60 * 60_000\n\n/**\n * 비교할 과거 창들 — 현재 창을 **같은 길이로** 하루(또는 한 주)씩 뒤로 옮긴다.\n *\n * 가까운 것부터 준다(어제 → 그제 → …). 화면이 시간 순서를 다시 정하지 않아도 되게.\n *\n * **한계를 밝힌다**: 옮기는 것은 ms 산술이다. 트윈 공간은 고정 오프셋(`utcOffsetMinutes`)을 선언하므로\n * 현재 모델에서는 \"같은 시간대\" 가 정확하다. 일광절약시간을 쓰는 시간대를 나중에 지원하면 전환이 있는\n * 주의 하루가 한 시간 밀린다 — 그때는 오프셋 기준 캘린더 산술로 바꿔야 한다(지금 지어내지 않는다).\n */\nexport function baselineWindows(\n fromMs: number,\n toMs: number,\n periods: number,\n stride: BaselineStride = 'day'\n): BaselineWindow[] {\n if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs <= fromMs) return []\n const count = Math.max(0, Math.min(MAX_BASELINE_PERIODS, Math.floor(periods)))\n const step = stride === 'week' ? 7 * MS_DAY : MS_DAY\n\n return Array.from({ length: count }, (_, i) => ({\n fromMs: fromMs - step * (i + 1),\n toMs: toMs - step * (i + 1)\n }))\n}\n\n/** 지표의 성질 — 이름과 값 꺼내기는 호출부(`TWIN_METRIC`)가 소유한다. */\nexport interface MetricSpecLike {\n direction: 'higher' | 'lower'\n unit: string\n}\n\n/** 과거 창 하나에서 꺼낸 값들 — 그 창에 기록이 없으면 값이 `undefined` 다. */\nexport interface BaselineSampleInput<M extends string> {\n fromMs: number\n toMs: number\n values: Partial<Record<M, number | undefined>>\n}\n\n/** 과거 창 하나의 결과 — **결측도 남긴다**(빼면 \"지난 7일\" 이 거짓이 된다). */\nexport interface BaselineSample {\n fromMs: number\n toMs: number\n measured: boolean\n value?: number\n}\n\n/** 왜 자리를 말하지 않는지. */\nexport type BaselineNoPosition = 'no-current' | 'too-few-samples'\n\n/**\n * 분포 안에서 현재의 자리 — **방향을 적용한 읽기.**\n *\n * 임의의 임계가 없다: 표본 전부보다 좋으면 `best`, 전부보다 나쁘면 `worst`, 그 사이는 중앙값을 기준으로\n * 구분한다. `typical` 은 중앙값과 **같을 때**만이다.\n */\nexport type BaselineStanding = 'best' | 'better-than-typical' | 'typical' | 'worse-than-typical' | 'worst'\n\nexport interface MetricBaseline<M extends string = string> {\n metric: M\n direction: 'higher' | 'lower'\n unit: string\n /** 지금 창의 값 — 없으면 `undefined`(0 이 아니다). */\n current?: number\n /** 요청한 과거 창 전부(결측 포함, 가까운 것부터). */\n samples: BaselineSample[]\n /** 요청한 창 수 / 그중 실제로 측정된 수 — 둘을 함께 봐야 \"지난 7일\" 을 믿을 수 있다. */\n requested: number\n measuredCount: number\n /** 측정된 표본만의 요약. 표본이 없으면 전부 `undefined`. */\n min?: number\n median?: number\n max?: number\n /** 측정 표본을 오름차순으로 놓았을 때 현재 값이 몇 번째인가(1 = 가장 작음) — **방향 없는 사실**. */\n rank?: number\n /** 그 순위를 셀 때의 표본 수(+현재 1). 순위만 보면 표본이 둘이었는지 열이었는지 알 수 없다. */\n rankOf?: number\n /** 방향을 적용한 읽기. 말할 수 없으면 없다. */\n standing?: BaselineStanding\n /** 자리를 말하지 않은 이유. */\n noPosition?: BaselineNoPosition\n}\n\n/** 중앙값 — 짝수 개면 가운데 둘의 평균(폴드의 분위수 규약과 달리 여기 표본은 아주 작다). */\nfunction medianOf(sorted: readonly number[]): number | undefined {\n if (!sorted.length) return undefined\n const mid = sorted.length >> 1\n return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2\n}\n\n/**\n * 지표별로 분포와 자리를 만든다.\n *\n * @param specs 지표 성질(방향·단위). 열거된 지표만 낸다 — 모르는 이름을 짐작하지 않는다.\n * @param current 지금 창에서 꺼낸 값들(결측은 `undefined`).\n * @param inputs 과거 창들에서 꺼낸 값들(가까운 것부터).\n */\nexport function summarizeBaseline<M extends string>(\n specs: Readonly<Record<M, MetricSpecLike>>,\n current: Partial<Record<M, number | undefined>>,\n inputs: readonly BaselineSampleInput<M>[]\n): MetricBaseline<M>[] {\n const out: MetricBaseline<M>[] = []\n\n for (const metric of Object.keys(specs) as M[]) {\n const spec = specs[metric]\n const samples: BaselineSample[] = inputs.map(s => {\n const v = s.values[metric]\n const measured = typeof v === 'number' && Number.isFinite(v)\n return measured ? { fromMs: s.fromMs, toMs: s.toMs, measured: true, value: v as number } : { fromMs: s.fromMs, toMs: s.toMs, measured: false }\n })\n\n const values = samples.filter(s => s.measured).map(s => s.value as number)\n const sorted = [...values].sort((a, b) => a - b)\n const cur = current[metric]\n const hasCurrent = typeof cur === 'number' && Number.isFinite(cur)\n\n const row: MetricBaseline<M> = {\n metric,\n direction: spec.direction,\n unit: spec.unit,\n ...(hasCurrent ? { current: cur as number } : {}),\n samples,\n requested: inputs.length,\n measuredCount: values.length,\n ...(sorted.length ? { min: sorted[0], median: medianOf(sorted), max: sorted[sorted.length - 1] } : {})\n }\n\n /* 자리를 말할 수 없는 두 경우 — 이유를 밝힌다(들어온 것이 없는 빈 값이 \"평범함\" 으로 읽힌다). */\n if (!hasCurrent) row.noPosition = 'no-current'\n else if (sorted.length < 2) row.noPosition = 'too-few-samples'\n else {\n const value = cur as number\n const median = medianOf(sorted) as number\n /* 오름차순에서 몇 번째인가 — 같은 값은 뒤에 놓는다(동률이면 \"더 큰 쪽\" 으로 세지 않는다). */\n row.rank = sorted.filter(v => v < value).length + 1\n row.rankOf = sorted.length + 1\n\n const higherIsBetter = spec.direction === 'higher'\n const beatsAll = higherIsBetter ? value > sorted[sorted.length - 1] : value < sorted[0]\n const losesToAll = higherIsBetter ? value < sorted[0] : value > sorted[sorted.length - 1]\n /* 중앙값보다 큰 것이 \"좋은\" 쪽인가 — 방향을 여기 한 번만 적용한다. */\n const aboveMedianIsBetter = higherIsBetter === value > median\n\n row.standing = beatsAll\n ? 'best'\n : losesToAll\n ? 'worst'\n : value === median\n ? 'typical'\n : aboveMedianIsBetter\n ? 'better-than-typical'\n : 'worse-than-typical'\n }\n\n out.push(row)\n }\n\n return out\n}\n"]}