@things-factory/headless-twin 10.0.12 โ†’ 10.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/dist-server/engine/canonical-ingest.d.ts +57 -2
  2. package/dist-server/engine/canonical-ingest.js +43 -2
  3. package/dist-server/engine/canonical-ingest.js.map +1 -1
  4. package/dist-server/engine/command-routing.js +1 -1
  5. package/dist-server/engine/command-routing.js.map +1 -1
  6. package/dist-server/engine/index.d.ts +3 -0
  7. package/dist-server/engine/index.js +4 -0
  8. package/dist-server/engine/index.js.map +1 -1
  9. package/dist-server/engine/ingest-health.d.ts +335 -0
  10. package/dist-server/engine/ingest-health.js +434 -0
  11. package/dist-server/engine/ingest-health.js.map +1 -0
  12. package/dist-server/engine/integration-coverage.d.ts +76 -0
  13. package/dist-server/engine/integration-coverage.js +73 -0
  14. package/dist-server/engine/integration-coverage.js.map +1 -0
  15. package/dist-server/engine/integration-probes.d.ts +65 -0
  16. package/dist-server/engine/integration-probes.js +100 -0
  17. package/dist-server/engine/integration-probes.js.map +1 -0
  18. package/dist-server/engine/integration-runner.d.ts +45 -0
  19. package/dist-server/engine/integration-runner.js +59 -0
  20. package/dist-server/engine/integration-runner.js.map +1 -0
  21. package/dist-server/engine/integration-target-profile.d.ts +57 -0
  22. package/dist-server/engine/integration-target-profile.js +79 -0
  23. package/dist-server/engine/integration-target-profile.js.map +1 -0
  24. package/dist-server/engine/kpi-fold.d.ts +27 -0
  25. package/dist-server/engine/kpi-fold.js +47 -4
  26. package/dist-server/engine/kpi-fold.js.map +1 -1
  27. package/dist-server/engine/kpi-query.js +128 -14
  28. package/dist-server/engine/kpi-query.js.map +1 -1
  29. package/dist-server/engine/live-feed-registry.js +2 -16
  30. package/dist-server/engine/live-feed-registry.js.map +1 -1
  31. package/dist-server/engine/load-meter.d.ts +1 -1
  32. package/dist-server/engine/load-meter.js +12 -3
  33. package/dist-server/engine/load-meter.js.map +1 -1
  34. package/dist-server/engine/log.d.ts +18 -0
  35. package/dist-server/engine/log.js +80 -0
  36. package/dist-server/engine/log.js.map +1 -0
  37. package/dist-server/engine/loop-lag.d.ts +54 -0
  38. package/dist-server/engine/loop-lag.js +87 -0
  39. package/dist-server/engine/loop-lag.js.map +1 -0
  40. package/dist-server/engine/model-gap.d.ts +108 -0
  41. package/dist-server/engine/model-gap.js +95 -0
  42. package/dist-server/engine/model-gap.js.map +1 -0
  43. package/dist-server/engine/restart-policy.d.ts +3 -3
  44. package/dist-server/engine/restart-policy.js +4 -4
  45. package/dist-server/engine/restart-policy.js.map +1 -1
  46. package/dist-server/engine/runtime-key.js +1 -1
  47. package/dist-server/engine/runtime-key.js.map +1 -1
  48. package/dist-server/engine/stage-path.d.ts +83 -0
  49. package/dist-server/engine/stage-path.js +118 -0
  50. package/dist-server/engine/stage-path.js.map +1 -0
  51. package/dist-server/engine/twin-engine.d.ts +352 -16
  52. package/dist-server/engine/twin-engine.js +1037 -166
  53. package/dist-server/engine/twin-engine.js.map +1 -1
  54. package/dist-server/index.js +34 -2
  55. package/dist-server/index.js.map +1 -1
  56. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.d.ts +5 -0
  57. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js +58 -0
  58. package/dist-server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.js.map +1 -0
  59. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.d.ts +5 -0
  60. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js +55 -0
  61. package/dist-server/migrations/1786200000000-CarryLiveFeedCursor.js.map +1 -0
  62. package/dist-server/migrations/index.js +7 -1
  63. package/dist-server/migrations/index.js.map +1 -1
  64. package/dist-server/service/index.d.ts +5 -2
  65. package/dist-server/service/index.js +18 -7
  66. package/dist-server/service/index.js.map +1 -1
  67. package/dist-server/service/reference/discovery-result.d.ts +1 -1
  68. package/dist-server/service/reference/discovery-result.js +1 -1
  69. package/dist-server/service/reference/discovery-result.js.map +1 -1
  70. package/dist-server/service/reference/index.d.ts +4 -1
  71. package/dist-server/service/reference/index.js +6 -1
  72. package/dist-server/service/reference/index.js.map +1 -1
  73. package/dist-server/service/reference/reference-adapter.d.ts +184 -3
  74. package/dist-server/service/reference/reference-adapter.js +28 -4
  75. package/dist-server/service/reference/reference-adapter.js.map +1 -1
  76. package/dist-server/service/reference/reference-assessment.d.ts +68 -0
  77. package/dist-server/service/reference/reference-assessment.js +136 -0
  78. package/dist-server/service/reference/reference-assessment.js.map +1 -0
  79. package/dist-server/service/reference/reference-live.d.ts +12 -1
  80. package/dist-server/service/reference/reference-live.js +116 -11
  81. package/dist-server/service/reference/reference-live.js.map +1 -1
  82. package/dist-server/service/reference/reference-master.d.ts +47 -1
  83. package/dist-server/service/reference/reference-master.js +34 -4
  84. package/dist-server/service/reference/reference-master.js.map +1 -1
  85. package/dist-server/service/reference/reference-probe.d.ts +92 -0
  86. package/dist-server/service/reference/reference-probe.js +186 -0
  87. package/dist-server/service/reference/reference-probe.js.map +1 -0
  88. package/dist-server/service/reference/reference-progress-subscription.d.ts +17 -0
  89. package/dist-server/service/reference/reference-progress-subscription.js +94 -0
  90. package/dist-server/service/reference/reference-progress-subscription.js.map +1 -0
  91. package/dist-server/service/reference/reference-progress.d.ts +38 -0
  92. package/dist-server/service/reference/reference-progress.js +71 -0
  93. package/dist-server/service/reference/reference-progress.js.map +1 -0
  94. package/dist-server/service/reference/reference-resolver.js +210 -43
  95. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  96. package/dist-server/service/reference/twin-reference.d.ts +32 -0
  97. package/dist-server/service/reference/twin-reference.js +10 -0
  98. package/dist-server/service/reference/twin-reference.js.map +1 -1
  99. package/dist-server/service/twin-control/twin-control-mutation.js +4 -3
  100. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  101. package/dist-server/service/twin-event/journal-count.d.ts +21 -0
  102. package/dist-server/service/twin-event/journal-count.js +26 -0
  103. package/dist-server/service/twin-event/journal-count.js.map +1 -0
  104. package/dist-server/service/twin-event/twin-event-keys.d.ts +9 -0
  105. package/dist-server/service/twin-event/twin-event-keys.js +15 -18
  106. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  107. package/dist-server/service/twin-event/twin-event-type.d.ts +3 -1
  108. package/dist-server/service/twin-event/twin-event-type.js +18 -1
  109. package/dist-server/service/twin-event/twin-event-type.js.map +1 -1
  110. package/dist-server/service/twin-event/twin-event.d.ts +1 -0
  111. package/dist-server/service/twin-event/twin-event.js +9 -1
  112. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  113. package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
  114. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  115. package/dist-server/service/twin-ingest-window/index.d.ts +5 -0
  116. package/dist-server/service/twin-ingest-window/index.js +10 -0
  117. package/dist-server/service/twin-ingest-window/index.js.map +1 -0
  118. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.d.ts +12 -0
  119. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js +123 -0
  120. package/dist-server/service/twin-ingest-window/twin-ingest-window-query.js.map +1 -0
  121. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.d.ts +1 -0
  122. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js +51 -0
  123. package/dist-server/service/twin-ingest-window/twin-ingest-window-writer.js.map +1 -0
  124. package/dist-server/service/twin-ingest-window/twin-ingest-window.d.ts +16 -0
  125. package/dist-server/service/twin-ingest-window/twin-ingest-window.js +108 -0
  126. package/dist-server/service/twin-ingest-window/twin-ingest-window.js.map +1 -0
  127. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  128. package/dist-server/service/twin-journal/twin-journal-query.js +58 -5
  129. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  130. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +14 -0
  131. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +72 -2
  132. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  133. package/dist-server/service/twin-metrics/twin-metrics-query.d.ts +1 -0
  134. package/dist-server/service/twin-metrics/twin-metrics-query.js +78 -3
  135. package/dist-server/service/twin-metrics/twin-metrics-query.js.map +1 -1
  136. package/dist-server/service/twin-model/axis-journal-evidence.d.ts +2 -0
  137. package/dist-server/service/twin-model/axis-journal-evidence.js +89 -0
  138. package/dist-server/service/twin-model/axis-journal-evidence.js.map +1 -0
  139. package/dist-server/service/twin-model/epcis-coverage.d.ts +1 -1
  140. package/dist-server/service/twin-model/epcis-coverage.js +19 -7
  141. package/dist-server/service/twin-model/epcis-coverage.js.map +1 -1
  142. package/dist-server/service/twin-model/iec61850-coverage.d.ts +1 -1
  143. package/dist-server/service/twin-model/iec61850-coverage.js +18 -7
  144. package/dist-server/service/twin-model/iec61850-coverage.js.map +1 -1
  145. package/dist-server/service/twin-model/isa95-coverage.d.ts +14 -0
  146. package/dist-server/service/twin-model/isa95-coverage.js +23 -9
  147. package/dist-server/service/twin-model/isa95-coverage.js.map +1 -1
  148. package/dist-server/service/twin-model/item-ref.d.ts +24 -0
  149. package/dist-server/service/twin-model/item-ref.js +88 -0
  150. package/dist-server/service/twin-model/item-ref.js.map +1 -0
  151. package/dist-server/service/twin-model/name-index.d.ts +36 -0
  152. package/dist-server/service/twin-model/name-index.js +116 -0
  153. package/dist-server/service/twin-model/name-index.js.map +1 -0
  154. package/dist-server/service/twin-model/project-structure.js +1 -1
  155. package/dist-server/service/twin-model/project-structure.js.map +1 -1
  156. package/dist-server/service/twin-model/status-tally.d.ts +9 -0
  157. package/dist-server/service/twin-model/status-tally.js +37 -0
  158. package/dist-server/service/twin-model/status-tally.js.map +1 -0
  159. package/dist-server/service/twin-model/twin-lineage-query.js +40 -12
  160. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  161. package/dist-server/service/twin-model/twin-model-item-query.js +38 -39
  162. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  163. package/dist-server/service/twin-model/twin-model-query.js +156 -4
  164. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  165. package/dist-server/service/twin-model/twin-model-tree-query.js +7 -0
  166. package/dist-server/service/twin-model/twin-model-tree-query.js.map +1 -1
  167. package/dist-server/service/twin-readiness/index.d.ts +2 -0
  168. package/dist-server/service/twin-readiness/index.js +6 -0
  169. package/dist-server/service/twin-readiness/index.js.map +1 -0
  170. package/dist-server/service/twin-readiness/twin-readiness-query.d.ts +3 -0
  171. package/dist-server/service/twin-readiness/twin-readiness-query.js +103 -0
  172. package/dist-server/service/twin-readiness/twin-readiness-query.js.map +1 -0
  173. package/dist-server/service/twin-space/twin-space-resolver.d.ts +2 -2
  174. package/dist-server/service/twin-space/twin-space-resolver.js +5 -4
  175. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  176. package/dist-shared/entity-delta.d.ts +23 -3
  177. package/dist-shared/entity-delta.js +12 -8
  178. package/dist-shared/entity-delta.js.map +1 -1
  179. package/dist-shared/kpi-broadcast.js +1 -1
  180. package/dist-shared/kpi-broadcast.js.map +1 -1
  181. package/dist-shared/touched-items.d.ts +7 -0
  182. package/dist-shared/touched-items.js +80 -0
  183. package/dist-shared/touched-items.js.map +1 -0
  184. package/package.json +7 -7
  185. package/server/engine/canonical-ingest.ts +92 -7
  186. package/server/engine/command-routing.ts +1 -1
  187. package/server/engine/index.ts +4 -0
  188. package/server/engine/ingest-health.ts +704 -0
  189. package/server/engine/integration-coverage.ts +147 -0
  190. package/server/engine/integration-probes.ts +144 -0
  191. package/server/engine/integration-runner.ts +95 -0
  192. package/server/engine/integration-target-profile.ts +103 -0
  193. package/server/engine/kpi-fold.ts +72 -4
  194. package/server/engine/kpi-query.ts +129 -15
  195. package/server/engine/live-feed-registry.ts +2 -1
  196. package/server/engine/load-meter.ts +12 -3
  197. package/server/engine/log.ts +72 -0
  198. package/server/engine/loop-lag.ts +120 -0
  199. package/server/engine/model-gap.ts +168 -0
  200. package/server/engine/restart-policy.ts +4 -4
  201. package/server/engine/runtime-key.ts +1 -1
  202. package/server/engine/stage-path.ts +172 -0
  203. package/server/engine/twin-engine.ts +1135 -162
  204. package/server/index.ts +35 -2
  205. package/server/migrations/1786100000000-PromoteEventActionAndSyncWarnings.ts +54 -0
  206. package/server/migrations/1786200000000-CarryLiveFeedCursor.ts +53 -0
  207. package/server/migrations/index.ts +7 -1
  208. package/server/service/index.ts +11 -0
  209. package/server/service/reference/discovery-result.ts +1 -1
  210. package/server/service/reference/index.ts +6 -1
  211. package/server/service/reference/reference-adapter.ts +197 -5
  212. package/server/service/reference/reference-assessment.ts +215 -0
  213. package/server/service/reference/reference-live.ts +123 -11
  214. package/server/service/reference/reference-master.ts +64 -4
  215. package/server/service/reference/reference-probe.ts +264 -0
  216. package/server/service/reference/reference-progress-subscription.ts +73 -0
  217. package/server/service/reference/reference-progress.ts +95 -0
  218. package/server/service/reference/reference-resolver.ts +200 -12
  219. package/server/service/reference/twin-reference.ts +39 -1
  220. package/server/service/twin-control/twin-control-mutation.ts +4 -3
  221. package/server/service/twin-event/journal-count.ts +53 -0
  222. package/server/service/twin-event/twin-event-keys.ts +16 -1
  223. package/server/service/twin-event/twin-event-type.ts +26 -2
  224. package/server/service/twin-event/twin-event.ts +7 -0
  225. package/server/service/twin-forecast/twin-forecast-query.ts +2 -1
  226. package/server/service/twin-ingest-window/index.ts +7 -0
  227. package/server/service/twin-ingest-window/twin-ingest-window-query.ts +125 -0
  228. package/server/service/twin-ingest-window/twin-ingest-window-writer.ts +57 -0
  229. package/server/service/twin-ingest-window/twin-ingest-window.ts +113 -0
  230. package/server/service/twin-instance/twin-instance.ts +1 -1
  231. package/server/service/twin-journal/twin-journal-query.ts +59 -5
  232. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +77 -3
  233. package/server/service/twin-metrics/twin-metrics-query.ts +81 -3
  234. package/server/service/twin-model/axis-journal-evidence.ts +57 -0
  235. package/server/service/twin-model/epcis-coverage.ts +2 -8
  236. package/server/service/twin-model/iec61850-coverage.ts +2 -8
  237. package/server/service/twin-model/isa95-coverage.ts +22 -9
  238. package/server/service/twin-model/item-ref.ts +80 -0
  239. package/server/service/twin-model/name-index.ts +94 -0
  240. package/server/service/twin-model/project-structure.ts +1 -1
  241. package/server/service/twin-model/status-tally.ts +37 -0
  242. package/server/service/twin-model/twin-lineage-query.ts +38 -9
  243. package/server/service/twin-model/twin-model-item-query.ts +26 -27
  244. package/server/service/twin-model/twin-model-query.ts +155 -4
  245. package/server/service/twin-model/twin-model-tree-query.ts +7 -0
  246. package/server/service/twin-readiness/index.ts +3 -0
  247. package/server/service/twin-readiness/twin-readiness-query.ts +96 -0
  248. package/server/service/twin-space/twin-space-resolver.ts +5 -4
  249. package/shared/entity-delta.ts +31 -7
  250. package/shared/kpi-broadcast.ts +1 -1
  251. package/shared/touched-items.ts +73 -0
  252. package/test/axis-journal-evidence.test.ts +71 -0
  253. package/test/axis-read.test.ts +101 -1
  254. package/test/boot-resume.test.ts +92 -26
  255. package/test/broadcast-cost-baseline.test.ts +200 -0
  256. package/test/broadcast-period.test.ts +58 -0
  257. package/test/canonical-ingest-vocabularies.test.ts +36 -1
  258. package/test/canonical-quantity-door.test.ts +61 -0
  259. package/test/command-routing.test.ts +1 -1
  260. package/test/declared-location-types.test.ts +89 -0
  261. package/test/discovery-result.test.ts +1 -1
  262. package/test/duration-estimators.test.ts +1 -1
  263. package/test/entity-delta.test.ts +2 -2
  264. package/test/ingest-bench.test.ts +3 -3
  265. package/test/ingest-health-engine.test.ts +248 -0
  266. package/test/ingest-health-wiring.test.ts +119 -0
  267. package/test/ingest-health.test.ts +306 -0
  268. package/test/ingest-history.test.ts +247 -0
  269. package/test/integration-probes.test.ts +103 -0
  270. package/test/integration-runner.test.ts +95 -0
  271. package/test/item-ref.test.ts +78 -0
  272. package/test/journal-read-discipline.test.ts +177 -0
  273. package/test/journal-retention.test.ts +133 -0
  274. package/test/journal-sort-axis.test.ts +142 -0
  275. package/test/journal-write-door.test.ts +110 -0
  276. package/test/journal-write-trend.test.ts +115 -0
  277. package/test/kernel-kind-guard.test.ts +4 -4
  278. package/test/kpi-fold.test.ts +90 -3
  279. package/test/kpi-query-bench.test.ts +2 -2
  280. package/test/lineage-survives-restart.test.ts +17 -1
  281. package/test/live-cursor-wiring.test.ts +87 -0
  282. package/test/live-feed-registry.test.ts +9 -1
  283. package/test/live-kernel-facts.test.ts +7 -7
  284. package/test/live-mirror-parity.test.ts +6 -0
  285. package/test/load-meter.test.ts +29 -15
  286. package/test/local-declarations.test.ts +1 -1
  287. package/test/log-stamp.test.ts +59 -0
  288. package/test/loop-lag.test.ts +82 -0
  289. package/test/model-gap.test.ts +153 -0
  290. package/test/oee-accumulator.test.ts +4 -0
  291. package/test/operational-vocabulary.test.ts +4 -4
  292. package/test/read-failure-visible.test.ts +145 -0
  293. package/test/reference-grounding.test.ts +70 -0
  294. package/test/resolve-ts-siblings.mjs +52 -0
  295. package/test/restart-policy.test.ts +7 -7
  296. package/test/revision-axis.test.ts +93 -0
  297. package/test/runtime-key.test.ts +2 -2
  298. package/test/scale-twin-bench.test.ts +2 -2
  299. package/test/spec-coverage.test.ts +1 -1
  300. package/test/stage-path.test.ts +95 -0
  301. package/test/standard-coverage.test.ts +15 -3
  302. package/test/status-tally.test.ts +55 -0
  303. package/test/structure-revision-db.test.ts +4 -0
  304. package/test/tenant-registry-db.test.ts +1 -1
  305. package/test/time-range.test.ts +152 -0
  306. package/test/touched-items.test.ts +61 -0
  307. package/test/twin-event-keys.test.ts +10 -2
  308. package/test/twin-model-item-db.test.ts +2 -0
  309. package/test/warm-start-seam.test.ts +4 -0
  310. package/test/yield-loop.test.ts +57 -2
  311. package/tsconfig.shared.tsbuildinfo +1 -1
  312. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,59 @@
1
+ /*
2
+ * ๋กœ๊ทธ์— **์‹œ๊ฐ์ด ์žˆ๋‹ค** โ€” ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ค ์ธ์ž๋„ ๋ฒ„๋ฆฌ์ง€ ์•Š๋Š”๋‹ค (2026-08-22).
3
+ *
4
+ * ๋ฃจํ”„๊ฐ€ 55์ดˆ ๋ง‰ํžŒ ๊ฒƒ์„ ๊ณ„๊ธฐ๋กœ ์•Œ์•„๋ƒˆ๋Š”๋ฐ ๊ทธ ์‹œ๊ฐ์— ๋ฌด์Šจ ์ผ์ด ์žˆ์—ˆ๋Š”์ง€ ๋กœ๊ทธ์—์„œ ์ฐพ์„ ์ˆ˜ ์—†์—ˆ๋‹ค.
5
+ * ํŠธ์œˆ์˜ ์ค„์ด `console.log` ๋กœ ๋‚˜๊ฐ€ ์‹œ๊ฐ์ด ์—†์—ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ด ์‹œํ—˜์ด ์ง€ํ‚ค๋Š” ๊ฒƒ:
6
+ * โ‘  ํ˜•์‹ โ€” ํ”„๋ ˆ์ž„์›Œํฌ ์ค„๊ณผ ๊ฐ™์€ ๋ชจ์–‘, **๋ฐ€๋ฆฌ์ดˆ๊นŒ์ง€**(๊ณ„๊ธฐ์˜ ๊ฐ’์ด epoch ms ๋‹ค)
7
+ * โ‘ก ์ง€์—ญ ์‹œ๊ฐ โ€” ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋‹ค๋ฅธ ์ค„๊ณผ ๊ฐ™์€ ๊ธฐ์ค€(UTC ๋กœ ์ฐ์œผ๋ฉด ๋‚˜๋ž€ํžˆ ์ฝ์„ ์ˆ˜ ์—†๋‹ค)
8
+ * โ‘ข ์ธ์ž๋ฅผ ๋ฒ„๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€” ์˜ค๋ฅ˜๋Š” ์Šคํƒ๊นŒ์ง€(`console.error(msg, err)` ๋กœ ์“ฐ๋˜ ์ž๋ฆฌ๊ฐ€ ๋งŽ๋‹ค)
9
+ * โ‘ฃ ํŠธ์œˆ ์†Œ์Šค์— `console.` ์ง์ ‘ ํ˜ธ์ถœ์ด ๋‚จ์ง€ ์•Š๋Š”๋‹ค โ€” ํ•˜๋‚˜๋ผ๋„ ๋‚จ์œผ๋ฉด ๊ทธ ์ค„๋งŒ ์‹œ๊ฐ์ด ์—†๋‹ค
10
+ */
11
+ import { test } from 'node:test'
12
+ import assert from 'node:assert/strict'
13
+ import { readFileSync, readdirSync } from 'node:fs'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ import { line, stamp } from '../server/engine/log.ts'
17
+
18
+ test('์‹œ๊ฐ์€ ๋ฐ€๋ฆฌ์ดˆ์™€ ์˜คํ”„์…‹๊นŒ์ง€ ์ ๋Š”๋‹ค โ€” ๊ฐ™์€ ์ดˆ ์•ˆ์˜ ์ˆœ์„œ๋ฅผ ๊ฐ€๋ฆด ์ˆ˜ ์žˆ๊ฒŒ', () => {
19
+ const s = stamp(new Date(2026, 7, 22, 5, 54, 12, 345))
20
+ assert.match(s, /^2026-08-22T05:54:12\.345[+-]\d\d:\d\d$/, `ํ˜•์‹์ด ๋‹ค๋ฅด๋‹ค: ${s}`)
21
+ })
22
+
23
+ test('์ง€์—ญ ์‹œ๊ฐ์œผ๋กœ ์ ๋Š”๋‹ค โ€” ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๋‹ค๋ฅธ ์ค„๊ณผ ๊ฐ™์€ ๊ธฐ์ค€', () => {
24
+ const at = new Date(2026, 0, 2, 3, 4, 5, 6)
25
+ const s = stamp(at)
26
+ assert.ok(s.startsWith('2026-01-02T03:04:05.006'), `์ง€์—ญ ์‹œ๊ฐ์ด ์•„๋‹ˆ๋‹ค: ${s}`)
27
+ })
28
+
29
+ test('์˜ค๋ฅ˜๋Š” ์Šคํƒ๊นŒ์ง€ ๋‚จ๋Š”๋‹ค โ€” ใ€Œ์–ด๋””์„œ ๋‚ฌ๋‚˜ใ€๋ฅผ ์žƒ์ง€ ์•Š๊ฒŒ', () => {
30
+ const err = new Error('๋ถˆ์ด ๋‚ฌ๋‹ค')
31
+ const out = line(['[twin-engine] ์ €์žฅ ์‹คํŒจ', err])
32
+ assert.ok(out.includes('[twin-engine] ์ €์žฅ ์‹คํŒจ'))
33
+ assert.ok(out.includes('๋ถˆ์ด ๋‚ฌ๋‹ค'))
34
+ assert.ok(out.includes('log-stamp.test'), '์Šคํƒ์ด ์žˆ์–ด์•ผ ์–ด๋А ์ž๋ฆฌ์—์„œ ๋‚ฌ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค')
35
+ })
36
+
37
+ test('๊ฐ’์ด ์—†์–ด๋„ ์žˆ์—ˆ๋‹ค๋Š” ์‚ฌ์‹ค์€ ๋‚จ๊ธด๋‹ค โ€” ์กฐ์šฉํžˆ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค', () => {
38
+ assert.equal(line(['a', undefined, null, 0]), 'a undefined null 0')
39
+ const circular: any = { name: 'ใ„ฑ' }
40
+ circular.self = circular
41
+ assert.ok(line(['๋ณด๊ณ ', circular]).startsWith('๋ณด๊ณ  '), '์ˆœํ™˜ ์ฐธ์กฐ์—๋„ ์ค„์ด ๋‚จ์•„์•ผ ํ•œ๋‹ค')
42
+ })
43
+
44
+ test('ํŠธ์œˆ ์†Œ์Šค์— console ์ง์ ‘ ํ˜ธ์ถœ์ด ๋‚จ์•„ ์žˆ์ง€ ์•Š๋‹ค โ€” ํ•œ ์ค„์ด๋ผ๋„ ๋‚จ์œผ๋ฉด ๊ทธ ์ค„๋งŒ ์‹œ๊ฐ์ด ์—†๋‹ค', () => {
45
+ const root = fileURLToPath(new URL('../server/', import.meta.url))
46
+ const offenders: string[] = []
47
+ const walk = (dir: string) => {
48
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
49
+ const p = dir + e.name
50
+ if (e.isDirectory()) walk(p + '/')
51
+ else if (e.name.endsWith('.ts') && e.name !== 'log.ts') {
52
+ const src = readFileSync(p, 'utf-8')
53
+ for (const m of src.matchAll(/console\.(log|warn|error)\(/g)) offenders.push(`${p.slice(root.length)} console.${m[1]}`)
54
+ }
55
+ }
56
+ }
57
+ walk(root)
58
+ assert.deepEqual(offenders, [], `์‹œ๊ฐ ์—†๋Š” ๋กœ๊ทธ๊ฐ€ ๋‚จ์•„ ์žˆ๋‹ค โ€” twinLogยทtwinWarnยทtwinError ๋ฅผ ์“ด๋‹ค:\n ${offenders.join('\n ')}`)
59
+ })
@@ -0,0 +1,82 @@
1
+ /*
2
+ * ๋ฃจํ”„ ์ง€์ฒด ๊ณ„๊ธฐ โ€” **์›์ธ์„ ์ง€์–ด๋‚ด์ง€ ์•Š๊ธฐ ์œ„ํ•œ ๊ฐ’**์ด ์ •ํ™•ํ•œ์ง€ ๋ณธ๋‹ค (2026-08-21).
3
+ *
4
+ * ์ง€ํ‚ค๋Š” ๊ฒƒ:
5
+ * โ‘  ๋Šฆ๊ฒŒ ๊นจ์–ด๋‚œ ๋งŒํผ๋งŒ ์ง€์ฒด๋‹ค โ€” ์ œ๋•Œ ๊นจ์–ด๋‚œ ํ‘œ๋ณธ์€ 0 ์ด๊ณ , 0 ์„ ์ •์ฒด๋กœ ์„ธ์ง€ ์•Š๋Š”๋‹ค.
6
+ * โ‘ก ์ฐฝ์ด ๋‹ซํžˆ๊ธฐ ์ „์—๋Š” `recent` ๊ฐ€ **null** ์ด๋‹ค(์ฑ„์šฐ๋Š” ์ค‘์ธ ์ฐฝ์„ ใ€Œ์ง€๊ธˆใ€์œผ๋กœ ์ฝ์œผ๋ฉด ๊ณผ์†Œ ์ง‘๊ณ„๋‹ค).
7
+ * โ‘ข ๊ฐ€์žฅ ๊ธธ๊ฒŒ ๋ง‰ํžŒ **์‹œ๊ฐ**์„ ๋“ ๋‹ค โ€” ์–ธ์ œ์˜€๋Š”์ง€๊ฐ€ ์›์ธ์„ ๊ฐ€๋ฆฌ๋Š” ๊ทผ๊ฑฐ๋‹ค.
8
+ * โ‘ฃ ๋ฌธํ„ฑ ์ดํ•˜์˜ ํ”๋“ค๋ฆผ์€ ์ •์ฒด๋กœ ์„ธ์ง€ ์•Š๋Š”๋‹ค.
9
+ */
10
+ import { test } from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+
13
+ import { LoopLagSampler } from '../server/engine/loop-lag.ts'
14
+
15
+ /** ์‹œ๊ณ„๋ฅผ ์†์œผ๋กœ ๋ฐ€์–ด ๋„ฃ๋Š”๋‹ค โ€” ์‹ค์ œ ํƒ€์ด๋จธ๋กœ ์žฌ๋ฉด ๊ธฐ๊ณ„ ์ƒํƒœ๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ์ •ํ•œ๋‹ค. */
16
+ function sampler(sampleMs = 500, stallMs = 200, windowMs = 10_000) {
17
+ let now = 1_000_000
18
+ const s = new LoopLagSampler(sampleMs, stallMs, windowMs, () => now)
19
+ return { s, advance: (ms: number) => (now += ms), at: () => now }
20
+ }
21
+
22
+ test('์ œ๋•Œ ๊นจ์–ด๋‚œ ํ‘œ๋ณธ์€ ์ง€์ฒด๊ฐ€ 0 ์ด๊ณ  ์ •์ฒด๋กœ ์„ธ์ง€ ์•Š๋Š”๋‹ค', () => {
23
+ const { s, advance, at } = sampler()
24
+ for (let i = 0; i < 4; i++) {
25
+ advance(500)
26
+ assert.equal(s.sample(at()), 0)
27
+ }
28
+ assert.equal(s.view().total.stalls, 0)
29
+ assert.equal(s.view().total.maxMs, 0)
30
+ assert.equal(s.view().total.samples, 4)
31
+ })
32
+
33
+ test('๋Šฆ๊ฒŒ ๊นจ์–ด๋‚œ ๋งŒํผ์ด ์ง€์ฒด๋‹ค โ€” ๊ทธ ์ดˆ๊ณผ๋ถ„๋งŒ', () => {
34
+ const { s, advance, at } = sampler()
35
+ advance(500)
36
+ s.sample(at())
37
+ advance(4_600) // ์˜ˆ์ •๋ณด๋‹ค 4,100ms ๋Šฆ๊ฒŒ
38
+ assert.equal(s.sample(at()), 4_100)
39
+ assert.equal(s.view().total.maxMs, 4_100)
40
+ assert.equal(s.view().total.stalls, 1)
41
+ })
42
+
43
+ test('๋ฌธํ„ฑ ์ดํ•˜์˜ ํ”๋“ค๋ฆผ์€ ์ •์ฒด๊ฐ€ ์•„๋‹ˆ๋‹ค โ€” ๊ฐ’์€ ๋‚จ๊ธฐ๊ณ  ์„ธ์ง€๋Š” ์•Š๋Š”๋‹ค', () => {
44
+ const { s, advance, at } = sampler(500, 200)
45
+ advance(650) // 150ms ์ง€์ฒด โ€” ๋ฌธํ„ฑ 200 ๋ฏธ๋งŒ
46
+ s.sample(at())
47
+ const v = s.view()
48
+ assert.equal(v.total.maxMs, 150)
49
+ assert.equal(v.total.stalls, 0, '๋ฌธํ„ฑ ์ดํ•˜๋ฅผ ์„ธ๋ฉด ์ •์ƒ ์ƒํƒœ๊ฐ€ ์ •์ฒด๋กœ ๋ณด์ธ๋‹ค')
50
+ })
51
+
52
+ test('์ฐฝ์ด ๋‹ซํžˆ๊ธฐ ์ „์—๋Š” recent ๊ฐ€ null ์ด๋‹ค โ€” ์ฑ„์šฐ๋Š” ์ค‘์ธ ์ฐฝ์„ ใ€Œ์ง€๊ธˆใ€์œผ๋กœ ์ฝ์ง€ ์•Š๋Š”๋‹ค', () => {
53
+ const { s, advance, at } = sampler(500, 200, 2_000)
54
+ advance(500)
55
+ s.sample(at())
56
+ assert.equal(s.view().recent, null)
57
+
58
+ /* ์ฐฝ ๊ธธ์ด๋ฅผ ๋„˜๊ธฐ๋ฉด ๊ทธ ์ฐฝ์ด ๋‹ซํ˜€ `recent` ๊ฐ€ ๋œ๋‹ค. */
59
+ advance(2_000)
60
+ s.sample(at())
61
+ const v = s.view()
62
+ assert.ok(v.recent, '์ฐฝ ๊ธธ์ด๋ฅผ ๋„˜๊ฒผ์œผ๋ฉด ๋‹ซํžŒ ์ฐฝ์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค')
63
+ assert.equal(v.recent!.stalls, 1, '1,500ms ์ง€์ฒด๋Š” ์ •์ฒด ํ•œ ๋ฒˆ์ด๋‹ค')
64
+ })
65
+
66
+ test('๊ฐ€์žฅ ๊ธธ๊ฒŒ ๋ง‰ํžŒ ์‹œ๊ฐ์„ ๋“ ๋‹ค โ€” ์–ธ์ œ์˜€๋Š”์ง€๊ฐ€ ์›์ธ์„ ๊ฐ€๋ฆฐ๋‹ค', () => {
67
+ const { s, advance, at } = sampler()
68
+ advance(500)
69
+ s.sample(at())
70
+ advance(3_000)
71
+ const worst = at()
72
+ s.sample(worst)
73
+ advance(500)
74
+ s.sample(at())
75
+ assert.equal(s.view().worstAtMs, worst)
76
+ })
77
+
78
+ test('ํ•œ ๋ฒˆ๋„ ์žฌ์ง€ ์•Š์•˜์œผ๋ฉด ์ •์ฒด ์‹œ๊ฐ์€ null ์ด๋‹ค โ€” 0 ์œผ๋กœ ์ ์ง€ ์•Š๋Š”๋‹ค', () => {
79
+ const { s } = sampler()
80
+ assert.equal(s.view().worstAtMs, null)
81
+ assert.equal(s.view().recent, null)
82
+ })
@@ -0,0 +1,153 @@
1
+ /*
2
+ * โ˜… ใ€Œ์›๋ณธ์ด ๋งํ•œ ์  ์—†๋Š” ๊ฒƒใ€์„ ์„ธ๋Š” ๊ทœ์œจ.
3
+ *
4
+ * โ”€โ”€ ์™œ ์ด๊ฒƒ์ด ํ•„์š”ํ•œ๊ฐ€ (2026-08-20 ์‹ค์ธก) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * ๋™๊ธฐํ™” ๊ฑด๊ฐ• ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค ๋•Œ ์ œ๊ฐ€ ์ „์ œ๋ฅผ ์ด๋ ‡๊ฒŒ ์ ์—ˆ๋‹ค: *ใ€Œ์ปค๋„ฅํ„ฐ์˜ ๋งคํ•‘์ด ํ•œ ๊ธ€์ž ํ‹€๋ฆฌ๋ฉด ํŠธ์œˆ์€
6
+ * ๋นˆ ๊ณต์žฅ์ด ๋œ๋‹คใ€*. ์‚ฌ์šฉ์ž๊ฐ€ ๊ทธ ์ „์ œ๋ฅผ ๊ฒ€์ฆํ•˜๊ฒŒ ํ•ด์„œ ์žฌ ๋ดค๋”๋‹ˆ, ์œ ์ž… ๋ฌธ์€ ์ฐธ์กฐ ์˜ค๋ฅ˜๋ฅผ **๊ฑฐ๋ถ€ํ•˜์ง€
7
+ * ์•Š์•˜๋‹ค**:
8
+ *
9
+ * ๋ชจ๋ฅด๋Š” ๋กœ์ผ€์ด์…˜ ใ€Œ์˜คํƒ€-99ใ€ โ†’ ํ†ต๊ณผ
10
+ * ๋ชจ๋ฅด๋Š” bizStep ใ€ŒPUTAWAY_X17ใ€ โ†’ ํ†ต๊ณผ
11
+ * ๋ชจ๋ฅด๋Š” ๊ณ„๋Ÿ‰๊ธฐ ใ€ŒMMXU-์˜คํƒ€-99ใ€ โ†’ ํ†ต๊ณผ
12
+ *
13
+ * ์ฆ‰ ๊ทธ ๊ธฐ๋Šฅ์ด ์žก๊ฒ ๋‹ค๊ณ  ๋‚ด์„ธ์šด ๋Œ€ํ‘œ ์‚ฌ๋ก€๊ฐ€ `mapping-blocked` ๋กœ๋Š” **๋œฐ ์ˆ˜ ์—†์—ˆ๋‹ค.** ๊ฑฐ๋ถ€๋˜๋Š” ๊ฒƒ์€
14
+ * ํ˜•ํƒœ ์˜ค๋ฅ˜๋ฟ์ด๋‹ค(์‹œ๊ฐ ํ˜•์‹ยท์ˆซ์ž ์•„๋‹Œ ๊ฐ’ยทepc ์—†์Œ).
15
+ *
16
+ * ๊ฑฐ๋ถ€ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ์˜ณ๋‹ค โ€” ์›์ฒœ์ด ๋ชจ๋ธ๋ณด๋‹ค ๋จผ์ € ์ƒˆ ๋กœ์ผ€์ด์…˜์„ ๋งํ•˜๋Š” ๊ฒƒ์€ ์ •์ƒ์ด๊ณ , ๊ฑฐ๋ถ€ํ•˜๋ฉด ๊ทธ ์„ ํ–‰
17
+ * ๊ณ„์ธก์„ ์žƒ๋Š”๋‹ค. ์ปค๋„์€ ํก์ˆ˜ํ•˜๊ณ  **ํ‘œ์‹œํ•œ๋‹ค**(`origin: 'observed'`). ๋น ์ ธ ์žˆ๋˜ ๊ฒƒ์€ ๊ทธ ํ‘œ์‹œ๋ฅผ
18
+ * ใ€Œ๋งž์ถฐ์ง€๊ณ  ์žˆ๋‚˜ใ€๋ฅผ ๋ณด๋Š” ์‚ฌ๋žŒ์—๊ฒŒ ์ž‡๋Š” ์ผ์ด์—ˆ๋‹ค.
19
+ *
20
+ * โ”€โ”€ ์ง€ํ‚ค๋Š” ๊ฒƒ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
21
+ * โ‘  ๋งˆ์Šคํ„ฐ๊ฐ€ ๋งํ•œ ๊ฒƒ๊ณผ ๊ด€์ธก์œผ๋กœ ์•Œ๊ฒŒ ๋œ ๊ฒƒ์„ ์„ž์ง€ ์•Š๋Š”๋‹ค
22
+ * โ‘ก ํŠธ์œˆ์ด ์•ˆ ๋Œ๋ฉด `null` โ€” ใ€Œ์—†๋‹คใ€์™€ ใ€Œ์„ธ์–ด ๋ณด์ง€ ๋ชปํ–ˆ๋‹คใ€๋Š” ๋‹ค๋ฅด๋‹ค
23
+ * โ‘ข ์กฐ์šฉํžˆ ์ž๋ฅด์ง€ ์•Š๋Š”๋‹ค โ€” ์ƒํ•œ์—์„œ ๋ฐ€๋ ค๋‚œ ์ˆ˜๋ฅผ ํ•จ๊ป˜ ๋‚ธ๋‹ค
24
+ * โ‘ฃ ๋ฎ์ง€ ๋ชปํ•˜๋Š” ์ถ•์„ ๋ฐํžŒ๋‹ค โ€” ๊ณ„๋Ÿ‰๊ธฐ๋Š” ์ปค๋„์ด ํ‘œ์‹œ๋ฅผ ๋‹ฌ์ง€ ์•Š์•„ ์ด ํ™”๋ฉด์ด ๋ชป ์ฐพ์•„ ์ค€๋‹ค
25
+ */
26
+ import { test } from 'node:test'
27
+ import assert from 'node:assert/strict'
28
+
29
+ import { GAP_ID_LIMIT, modelGapOf } from '../server/engine/model-gap.ts'
30
+
31
+ const loc = (id: string, origin: string) => ({ id, origin })
32
+
33
+ test('๋งˆ์Šคํ„ฐ๊ฐ€ ๋งํ•œ ๋กœ์ผ€์ด์…˜์€ ๊ฐญ์ด ์•„๋‹ˆ๋‹ค โ€” ์„ ์–ธ๋œ ๊ฒƒ์„ ๋ฌธ์ œ๋กœ ์„ธ์ง€ ์•Š๋Š”๋‹ค', () => {
34
+ const g = modelGapOf({
35
+ locations: [loc('dock-recv', 'master'), loc('bin-1', 'master')],
36
+ equipment: [loc('fk-1', 'master')]
37
+ })
38
+
39
+ assert.equal(g!.total, 0)
40
+ assert.deepEqual(g!.locations.ids, [])
41
+ })
42
+
43
+ test('๊ด€์ธก์œผ๋กœ๋งŒ ์•Œ๊ฒŒ ๋œ ๊ฒƒ์„ ์ถ•๋งˆ๋‹ค ์„ผ๋‹ค โ€” ์›์ฒœ์ด ์šฐ๋ฆฌ๊ฐ€ ๋ชจ๋ฅด๋Š” id ๋ฅผ ๋ณด๋‚ด๊ณ  ์žˆ๋‹ค', () => {
44
+ const g = modelGapOf({
45
+ locations: [loc('dock-recv', 'master'), loc('์˜คํƒ€-99', 'observed'), loc('A-77', 'observed')],
46
+ equipment: [loc('fk-1', 'master'), loc('์—†๋Š”-์ง€๊ฒŒ์ฐจ', 'observed')],
47
+ persons: [loc('unknown-op', 'observed')]
48
+ })
49
+
50
+ assert.equal(g!.locations.count, 2)
51
+ assert.equal(g!.locations.unknown, 0, 'ํ‘œ์‹œ๊ฐ€ ๋‹ค ์žˆ์œผ๋ฉด ๋ชจ๋ฆ„์€ 0')
52
+ assert.deepEqual(g!.locations.ids, ['์˜คํƒ€-99', 'A-77'], 'id ๋ฅผ ๋ณด์—ฌ ์ค€๋‹ค โ€” ์˜คํƒ€๋Š” ๋ˆˆ์œผ๋กœ ์ฐพ๋Š”๋‹ค')
53
+ assert.equal(g!.equipment.count, 1)
54
+ assert.equal(g!.persons.count, 1)
55
+ assert.equal(g!.total, 4, '์„ธ ์ถ•์˜ ํ•ฉ')
56
+ })
57
+
58
+ test('ํŠธ์œˆ์ด ์•ˆ ๋Œ๋ฉด ใ€Œ์—†์Œใ€์ด๋‹ค โ€” 0 ์ด ์•„๋‹ˆ๋‹ค', () => {
59
+ /* ๋น„๊ตํ•  ์ƒ๋Œ€๊ฐ€ ์—†๋‹ค. 0 ์œผ๋กœ ๋‹ตํ•˜๋ฉด ใ€Œ์›์ฒœ์ด ์šฐ๋ฆฌ๊ฐ€ ์•„๋Š” ๊ฒƒ๋งŒ ๋ณด๋‚ธ๋‹คใ€๋Š” ์—†๋Š” ์‚ฌ์‹ค์ด ์ƒ๊ธด๋‹ค. */
60
+ assert.equal(modelGapOf(null), null)
61
+ assert.equal(modelGapOf(undefined), null)
62
+ })
63
+
64
+ test('๋Œ์ง€๋งŒ ๊ด€์ธก ๊ฐญ์ด ์—†์œผ๋ฉด 0 ์ด๋‹ค โ€” ์ด๊ฑด ใ€Œ์—†์Œใ€๊ณผ ๋‹ค๋ฅด๋‹ค', () => {
65
+ const g = modelGapOf({ locations: [] })
66
+
67
+ assert.notEqual(g, null, '์„ธ์–ด ๋ดค๋‹ค')
68
+ assert.equal(g!.total, 0, '์„ธ์–ด ๋ณด๋‹ˆ ์—†์—ˆ๋‹ค')
69
+ })
70
+
71
+ test('id ๋ฅผ ์กฐ์šฉํžˆ ์ž๋ฅด์ง€ ์•Š๋Š”๋‹ค โ€” ๋ฐ€๋ ค๋‚œ ์ˆ˜๋ฅผ ํ•จ๊ป˜ ๋‚ธ๋‹ค', () => {
72
+ const many = Array.from({ length: GAP_ID_LIMIT + 7 }, (_, i) => loc(`ghost-${i}`, 'observed'))
73
+ const g = modelGapOf({ locations: many })
74
+
75
+ assert.equal(g!.locations.count, GAP_ID_LIMIT + 7, '์ „๋ถ€ ์„ผ๋‹ค')
76
+ assert.equal(g!.locations.ids.length, GAP_ID_LIMIT, '๋ณด์ด๋Š” ๊ฒƒ์€ ์ƒํ•œ๊นŒ์ง€')
77
+ assert.equal(g!.locations.more, 7, '๋ฐ€๋ ค๋‚œ ์ˆ˜๋ฅผ ๋งํ•œ๋‹ค')
78
+ })
79
+
80
+ test('์ปค๋„์ด ๋‹ด์„ ์ค„ ๋ชจ๋ฅธ ์‚ฌ๊ฑด์„ ํ•จ๊ป˜ ๋‚ธ๋‹ค โ€” ์ฐธ์กฐ ๋ฌธ์ œ์™€ ๋‹ค๋ฅธ ์‚ฌ์‹ค์ด๋‹ค', () => {
81
+ const at = Date.parse('2026-08-20T01:02:03.000Z')
82
+ const g = modelGapOf({
83
+ unhandled: [
84
+ { eventType: 'epcis.TransformationEvent', count: 12, lastAtMs: at },
85
+ { eventType: 'aggregation-mixed-classes', count: 3 }
86
+ ]
87
+ })
88
+
89
+ assert.equal(g!.unhandled.length, 2)
90
+ assert.equal(g!.unhandled[0].lastAt, '2026-08-20T01:02:03.000Z', '์–ธ์ œ๊นŒ์ง€ ๋‚ฌ๋Š”์ง€ โ€” ๋ฉˆ์ถ˜ ์ข…๋ฅ˜๋Š” ๊ณ ์ณ์ง„ ๊ฒƒ์ด๋‹ค')
91
+ assert.equal(g!.unhandled[1].lastAt, null, '์‹œ๊ฐ์ด ์—†์œผ๋ฉด ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค')
92
+ })
93
+
94
+ test('๋ชจ๋ฅด๋Š” ์„ค๋น„์˜ ์—๋„ˆ์ง€ ๋ณด๊ณ ๋Š” ใ€Œ์—†์Œใ€๊ณผ 0 ์„ ๊ฐ€๋ฅธ๋‹ค โ€” ๊ฒฝ๋กœ๊ฐ€ `energy` ์•ˆ์ด๋‹ค', () => {
95
+ /*
96
+ * ์ด ๊ฐ’์„ **๋ฃจํŠธ์—์„œ ์ฝ๊ณ  ์žˆ์—ˆ๋‹ค**(2026-08-20์— ์žก์Œ). ์ปค๋„์€ `EnergyState` ์•ˆ์— ๋‹ด์œผ๋ฏ€๋กœ ๋ฃจํŠธ์—์„œ
97
+ * ์ฝ์œผ๋ฉด ์˜์›ํžˆ `null` ์ด๊ณ , ์˜ค๋ฅ˜ ์—†์ด ใ€Œ์—๋„ˆ์ง€ ํŠธ์œˆ์ด ์•„๋‹ˆ๋‹คใ€๋กœ ์ฝํžŒ๋‹ค. ๋‚˜๋งŒ ์ฝ๋Š” ํ•„๋“œ๋ฅผ ์ž˜๋ชป๋œ
98
+ * ๊ฒฝ๋กœ์—์„œ ์ฝ์œผ๋ฉด ์ด๋ ‡๊ฒŒ ์กฐ์šฉํžˆ ์‚ฌ๋ผ์ง„๋‹ค.
99
+ *
100
+ * ๊ทธ๋ฆฌ๊ณ  ์ปค๋„์€ 0 ์ด๋ฉด ๊ทธ ์นธ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. ๊ทธ๋ž˜์„œ `energy` ์ž์ฒด๊ฐ€ ์žˆ๋Š”์ง€๋กœ ใ€Œ์„ธ์—ˆ๊ณ  0ใ€๊ณผ
101
+ * ใ€Œ์—๋„ˆ์ง€ ํŠธ์œˆ์ด ์•„๋‹˜ใ€์„ ๊ฐ€๋ฅธ๋‹ค.
102
+ */
103
+ assert.equal(modelGapOf({})!.unknownEquipmentReports, null, '์—๋„ˆ์ง€ ํŠธ์œˆ์ด ์•„๋‹ˆ๋ฉด ์ด ์ˆ˜๊ฐ€ ์—†๋‹ค')
104
+ assert.equal(modelGapOf({ energy: { points: [] } })!.unknownEquipmentReports, 0, '์—๋„ˆ์ง€ ํŠธ์œˆ์ด๋ฉด ์„ธ์—ˆ๊ณ  0 ์ด๋‹ค')
105
+ assert.equal(modelGapOf({ energy: { unknownEquipment: 41 } })!.unknownEquipmentReports, 41)
106
+ assert.equal(
107
+ modelGapOf({ unknownEquipment: 41 } as any)!.unknownEquipmentReports,
108
+ null,
109
+ '๋ฃจํŠธ์— ์žˆ๋Š” ๊ฐ’์„ ์ง‘์ง€ ์•Š๋Š”๋‹ค โ€” ๊ทธ ๊ฒฝ๋กœ๋Š” ์ปค๋„์˜ ๊ฒƒ์ด ์•„๋‹ˆ๋‹ค'
110
+ )
111
+ })
112
+
113
+ test('๊ณ„๋Ÿ‰๊ธฐ๋„ ๋ฎ๋Š”๋‹ค โ€” ์ด ์ถ•์ด ๊ฐ€์žฅ ๋ฌด๊ฒ๋‹ค', () => {
114
+ /*
115
+ * ๋ชจ๋ฅด๋Š” `meterId` ๋Š” ์กฐ์šฉํžˆ ์ƒˆ ๊ณ„๋Ÿ‰๊ธฐ๊ฐ€ ๋˜๊ณ  ๊ทธ kW ๊ฐ€ ๊ทธ ํ˜„์žฅ์˜ ์ˆ˜์š”ยทํ”ผํฌยท์š”๊ธˆ ํŒ์ •์— ๋”ํ•ด์ง„๋‹ค.
116
+ * ๋กœ์ผ€์ด์…˜ ์˜คํƒ€๋Š” ์Šคํ‚ค๋งคํ‹ฑ์ด ๋น„๋Š” ๊ฒƒ์œผ๋กœ ๋“œ๋Ÿฌ๋‚˜์ง€๋งŒ, ์ด๊ฑด **๊ทธ๋Ÿด๋“ฏํ•œ ์ˆซ์ž**๋กœ ๋‚˜์˜จ๋‹ค.
117
+ * ์ปค๋„ 0.7.39 ๊ฐ€ `MeterPointState.origin` ์„ ๋‚ด๋ฉด์„œ ๋ฎ์„ ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค.
118
+ */
119
+ const g = modelGapOf({
120
+ energy: { points: [loc('main-incoming', 'master'), loc('MMXU-์˜คํƒ€-99', 'observed')] }
121
+ })
122
+
123
+ assert.equal(g!.meters.count, 1)
124
+ assert.deepEqual(g!.meters.ids, ['MMXU-์˜คํƒ€-99'])
125
+ assert.equal(g!.total, 1, '๊ณ„๋Ÿ‰๊ธฐ๋„ ํ•ฉ์— ๋“ ๋‹ค')
126
+ assert.deepEqual(g!.notCovered, [], '๋ฎ์ง€ ๋ชปํ•˜๋Š” ์ถ•์ด ์—†๋‹ค')
127
+ })
128
+
129
+ test('ํ‘œ์‹œ๊ฐ€ ์—†๋Š” ํ•ญ๋ชฉ์€ ใ€Œ์„ ์–ธ๋จใ€์ด ์•„๋‹ˆ๋ผ ใ€Œ๋ชจ๋ฆ„ใ€์œผ๋กœ ์„ผ๋‹ค', () => {
130
+ /*
131
+ * ๊ณ„๋Ÿ‰๊ธฐ์˜ `origin` ์€ ๋Šฆ๊ฒŒ ์ƒ๊ธด ์นธ์ด๋ผ, ์˜› ์Šค๋ƒ…์ƒท์—์„œ ์ด์–ด๋ฐ›์€ ์ง€์ ์—๋Š” ์—†๋‹ค(`undefined`).
132
+ * ๋งˆ์Šคํ„ฐ๋กœ ์„ธ๋ฉด ์˜คํƒ€๋ฅผ ๋†“์น˜๊ณ , ๊ด€์ธก์œผ๋กœ ์„ธ๋ฉด ์ •์ƒ์„ ๋ฌธ์ œ๋กœ ๋งŒ๋“ ๋‹ค. ๋‘˜ ๋‹ค ํ‹€๋ฆฌ๋‹ค.
133
+ */
134
+ const g = modelGapOf({
135
+ energy: { points: [{ id: 'carried-over' }, loc('main', 'master'), loc('ghost', 'observed')] }
136
+ })
137
+
138
+ assert.equal(g!.meters.unknown, 1, 'ํ‘œ์‹œ ์—†๋Š” ๊ฒƒ์€ ๋ชจ๋ฆ„')
139
+ assert.equal(g!.meters.count, 1, '๊ด€์ธก ๊ฐญ์— ์„ž์ด์ง€ ์•Š๋Š”๋‹ค')
140
+ assert.ok(!g!.meters.ids.includes('carried-over'), '๋ชจ๋ฅด๋Š” ๊ฒƒ์„ ์˜คํƒ€๋กœ ์ง€๋ชฉํ•˜์ง€ ์•Š๋Š”๋‹ค')
141
+ })
142
+
143
+ test('`origin` ์€ ใ€Œ์ง€๊ธˆ ๋ชจ๋ธ์— ์žˆ๋Š”๊ฐ€ใ€์˜ ๋‹ต์ด๋‹ค โ€” ์ฑ„ํƒ๋˜๋ฉด ๊ฐญ์—์„œ ๋น ์ง„๋‹ค', () => {
144
+ /*
145
+ * ์ปค๋„์ด ํ‘œ๋ณธ๋งˆ๋‹ค ๋‹ค์‹œ ๋งž์ถ˜๋‹ค. ํ•œ ๋ฒˆ ์ฐ๊ณ  ๋‘๋ฉด ๋‚˜์ค‘์— ์„ ์–ธ๋œ ๊ณ„๋Ÿ‰๊ธฐ๊ฐ€ ์˜์›ํžˆ `observed` ๋กœ ๋‚จ๋Š”๋‹ค.
146
+ * ๊ทธ๋ž˜์„œ ์ฑ„ํƒ ๋’ค ์Šค๋ƒ…์ƒท์—์„œ๋Š” ๊ฐ™์€ id ๊ฐ€ `master` ๋กœ ์˜ค๊ณ , ์ด ๋„์ถœ์€ ๊ทธ๊ฒƒ์„ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ฅธ๋‹ค.
147
+ */
148
+ const before = modelGapOf({ energy: { points: [loc('new-meter', 'observed')] } })
149
+ const after = modelGapOf({ energy: { points: [loc('new-meter', 'master')] } })
150
+
151
+ assert.equal(before!.meters.count, 1)
152
+ assert.equal(after!.meters.count, 0, '๋ชจ๋ธ์— ๋“ค์–ด์˜ค๋ฉด ๊ฐญ์ด ์•„๋‹ˆ๋‹ค')
153
+ })
@@ -18,6 +18,10 @@ const TICK = 1000
18
18
  function runSimCollect() {
19
19
  const m = {
20
20
  source: 'oee', system: 'wms' as const, siteName: 'O',
21
+ /* ์ด ํ”ฝ์Šค์ฒ˜๊ฐ€ ์‹๋ณ„์ž๋ฅผ ๋งŒ๋“œ๋Š” ์ด๋ฆ„๊ณต๊ฐ„ โ€” **ํ”ฝ์Šค์ฒ˜๋งˆ๋‹ค ๋‹ค๋ฅด๊ฒŒ** ๋‘”๋‹ค(๊ฐ™์€ ์ ‘๋‘ ์•„๋ž˜์—์„œ ์„œ๋กœ ๋‹ค๋ฅธ
22
+ ์‹œํ—˜์˜ ๋ฒˆํ˜ธ๊ฐ€ ๋ถ€๋”ช์น˜๋ฉด ํ‘œ์ค€์˜ ์œ ์ผ์„ฑ ์š”๊ตฌ๋ฅผ ๊นฌ๋‹ค, CBV 2.0 ยง8.2.4). ์ปค๋„์€ ์„ ์–ธ ์—†์ด ํŒ”๋ ˆํŠธยท๋ฌธ์„œ๋ฅผ
23
+ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค(0.7.45 ์—์„œ ๋‚ ์กฐ ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ง€์› ๋‹ค). */
24
+ identity: { namespaces: ['https://twin.test/oee-accumulator'] },
21
25
  space: { width: 90, depth: 40, unit: 'm' },
22
26
  locations: [
23
27
  { id: 'a', type: 'warehouse', role: 'area' as const },
@@ -37,7 +37,7 @@ test('์šด์˜ ์‚ฌ์‹ค์ด ์ž๊ธฐ ๋ด‰ํˆฌ๋กœ ๋‚˜์˜จ๋‹ค โ€” EPCIS ๋กœ ๊ฐ์‹ธ์ง€์ง€ ์•Š
37
37
  test('๋„ค ์–ดํœ˜๊ฐ€ ํ•œ ๋ฐฐ์น˜์—์„œ ๊ฐˆ๋ฆฐ๋‹ค โ€” ์‹ค ํ”ผ๋“œ๋Š” ์„ž์—ฌ ์˜จ๋‹ค', () => {
38
38
  const { accepted, rejected } = ingestCanonicalRecords(
39
39
  [
40
- { epc: 'urn:epc:id:sgtin:1.1.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
40
+ { epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
41
41
  { meterId: 'm-1', kW: 120 },
42
42
  { equipmentId: 'pv-1', generatedKW: 40 },
43
43
  { moverId: 'fk-1', kind: 'forklift', status: 'busy' }, // vocabulary-guard: allow ์ €๋„ ์™€์ด์–ด ํ•„๋“œ
@@ -80,8 +80,8 @@ test('ํ”ผ๋“œ ๊ฒฐ์„ ์ด ๊ฑฐ๋ถ€๋ฅผ ์กฐ์šฉํžˆ ๋ฒ„๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€” ์ด์œ ๋ฅผ ์‚ฌ
80
80
  test('๋‹ด๊น€ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ž๊ธฐ ๋ด‰ํˆฌ๋กœ ๋‚˜์˜จ๋‹ค โ€” ๊ฐœ์ฒด ๊ด€์ธก๊ณผ ํ•œ ๋ฐฐ์น˜์— ์„ž์—ฌ ์™€๋„ ๊ฐˆ๋ฆฐ๋‹ค', () => {
81
81
  const { accepted, rejected } = ingestCanonicalRecords(
82
82
  [
83
- { epc: 'urn:epc:id:sgtin:1.1.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
84
- { parentID: 'urn:epc:id:sscc:0614141.0000000001', childEPCs: ['urn:epc:id:sgtin:1.1.1'], action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:packing', readPoint: 'packing' }
83
+ { epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: 'dock-1' },
84
+ { parentID: 'urn:epc:id:sscc:0614141.0000000001', childEPCs: ['urn:epc:id:sgtin:9521321.012345.1'], action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:packing', readPoint: 'packing' }
85
85
  ] as any,
86
86
  'domain-1',
87
87
  AT
@@ -93,7 +93,7 @@ test('๋‹ด๊น€ ๋ ˆ์ฝ”๋“œ๊ฐ€ ์ž๊ธฐ ๋ด‰ํˆฌ๋กœ ๋‚˜์˜จ๋‹ค โ€” ๊ฐœ์ฒด ๊ด€์ธก๊ณผ ํ•œ
93
93
 
94
94
  test('์ž๋ฆฌ๋ฅผ ๊ตฌ์กฐ์งธ ์‹ค์œผ๋ฉด ๊ฑฐ๋ถ€ํ•œ๋‹ค โ€” ์˜ˆ์ „์—๋Š” "[object Object]" ๋กœ ๊ตณ์–ด ์ €๋„์— ์Œ“์˜€๋‹ค', () => {
95
95
  const { accepted, rejected } = ingestCanonicalRecords(
96
- [{ epc: 'urn:epc:id:sgtin:1.1.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: { id: 'dock-1' } }] as any,
96
+ [{ epc: 'urn:epc:id:sgtin:9521321.012345.1', action: 'ADD', bizStep: 'urn:epcglobal:cbv:bizstep:receiving', readPoint: { id: 'dock-1' } }] as any,
97
97
  'domain-1',
98
98
  AT
99
99
  )
@@ -0,0 +1,145 @@
1
+ /*
2
+ * โ˜… ใ€Œ์›๋ณธ์— ๋‹ฟ์ง€ ๋ชปํ•œ๋‹คใ€์™€ ใ€Œ์›๋ณธ์ด ์กฐ์šฉํ•˜๋‹คใ€๋Š” **๋‹ค๋ฅธ ์‚ฌ์‹ค**์ด๋‹ค โ€” ์ƒํƒœ๊ฐ€ ๊ทธ๊ฒƒ์„ ๊ฐ€๋ ค์•ผ ํ•œ๋‹ค.
3
+ *
4
+ * โ”€โ”€ ๋ฌด์—‡์ด ์กฐ์šฉํ–ˆ๋‚˜ (2026-08-23 ์‹ค์ธก) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * ์‹ค ์›๋ณธ์ด ๋Š๊ฒผ๋‹ค(`jejoup.hatiolab.com:4000` ์ ‘์† ์‹œ๊ฐ„ ์ดˆ๊ณผ, HTTP 000 / 8์ดˆ). ๊ทธ๋Ÿฐ๋ฐ ํŠธ์œˆ์˜
6
+ * **์กฐํšŒ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ์–ด๋””์—๋„ ๊ทธ ์‚ฌ์‹ค์ด ์—†์—ˆ๋‹ค.**
7
+ *
8
+ * recordIngest `onRecords` ๊ฐ€ ๋ถˆ๋ฆด ๋•Œ๋งŒ ๋„๋‹ฌํ•œ๋‹ค โ€” ์ฝ๊ธฐ ์‹คํŒจ๋Š” ๊ทธ ๋ฌธ์— ์˜ค์ง€ ์•Š๋Š”๋‹ค
9
+ * TwinReference.lastError ๋งˆ์Šคํ„ฐ ๋™๊ธฐยท์ ‘์† ์‹œํ—˜์—๋งŒ ์ ํžŒ๋‹ค โ€” ๋ผ์ด๋ธŒ ํ”ผ๋“œ๋Š” ์ ์ง€ ์•Š์•˜๋‹ค
10
+ * ๊ณ„์•ฝ `openLiveFeed` ์— ใ€Œ์ฝ์ง€ ๋ชปํ–ˆ๋‹คใ€๋ฅผ ๋งํ•  ํ†ต๋กœ๊ฐ€ ์—†์—ˆ๋‹ค
11
+ *
12
+ * ์–ด๋Œ‘ํ„ฐ๋Š” **๋กœ๊ทธ๋กœ๋Š” ๋งํ•˜๊ณ  ์žˆ์—ˆ๋‹ค**(์žฌ์‹œ๋„ ๊ฒฝ๊ณ ). ๊ทธ๋Ÿฌ๋‚˜ ๋กœ๊ทธ๋Š” ์‚ฌ๋žŒ์ด ๋ณผ ๋•Œ๋งŒ ๊ฐ’์ด ์žˆ๋‹ค โ€” ํ™”๋ฉด์ด
13
+ * ๋งํ•˜๋ ค๋ฉด ์ƒํƒœ์— ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์‹ค์ฆ ๋ฐ๋ชจ ์ค‘์— ์›๋ณธ์ด ๋Š๊ธฐ๋ฉด ์‚ฌ์šฉ์ž๊ฐ€ ์›์ธ์„ ์ฐพ์„ ์ˆ˜ ์—†๋‹ค.
14
+ *
15
+ * โ”€โ”€ ์ด ์‹œํ—˜์ด ์ง€ํ‚ค๋Š” ๊ฒƒ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
16
+ * โ‘  ์‹คํŒจ๊ฐ€ ์ƒํƒœ์— ๋‚จ๋Š”๋‹ค โ‘ก ใ€Œ๋ช‡ ๋ฒˆใ€๊ณผ ใ€Œ์–ธ์ œ๋ถ€ํ„ฐใ€๋ฅผ ํ•จ๊ป˜ ๋“ ๋‹ค(ํ•œ ๋ฒˆ์˜ ํ”๋“ค๋ฆผ๊ณผ 20๋ถ„์งธ ๋‹จ์ ˆ์€ ๋‹ค๋ฅด๋‹ค)
17
+ * โ‘ข **๋นˆ ์ฝ๊ธฐ๋Š” ์„ฑ๊ณต์ด๋‹ค** โ€” ์กฐ์šฉํ•œ ์›๋ณธ์„ ๋Š๊ธด ์›๋ณธ์œผ๋กœ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค
18
+ * โ‘ฃ ์กฐํšŒ ๋ทฐ๋กœ ๋‚˜๊ฐ„๋‹ค โ€” ์ƒํƒœ์—๋งŒ ์žˆ๊ณ  ์•ˆ ๋‚˜๊ฐ€๋ฉด ํ™”๋ฉด์ด ๋ชป ๋ณธ๋‹ค
19
+ */
20
+ import { test } from 'node:test'
21
+ import assert from 'node:assert/strict'
22
+ import { readFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+ import {
25
+ clearReadFailure,
26
+ ingestHealth,
27
+ newIngestLedger,
28
+ recordIngest,
29
+ recordReadFailure
30
+ } from '../server/engine/ingest-health.ts'
31
+
32
+ const T0 = Date.parse('2026-08-23T06:00:00.000Z')
33
+
34
+ test('์‹คํŒจ๊ฐ€ ์ƒํƒœ์— ๋‚จ๋Š”๋‹ค โ€” ์‚ฌ์œ ์™€ ํ๋ฆ„๊นŒ์ง€', () => {
35
+ const l = newIngestLedger()
36
+ assert.equal(l.readFailure, undefined, '์ฒ˜์Œ์—๋Š” ์—†๋‹ค(์—†์Œ์ด ใ€Œ๋‹ฟ๋Š”๋‹คใ€๋Š” ๋œป์ด๋‹ค)')
37
+ recordReadFailure(l, 'connect ETIMEDOUT', T0, 'lot')
38
+ assert.equal(l.readFailure?.consecutive, 1)
39
+ assert.equal(l.readFailure?.reason, 'connect ETIMEDOUT')
40
+ assert.equal(l.readFailure?.stream, 'lot', '์–ด๋А ํ‘œ๊ฐ€ ๋ง‰ํ˜”๋‚˜๊ฐ€ ์›์ธ ์ฐพ๊ธฐ์˜ ์ ˆ๋ฐ˜์ด๋‹ค')
41
+ })
42
+
43
+ test('ํ•œ ๋ฒˆ์˜ ํ”๋“ค๋ฆผ๊ณผ 20๋ถ„์งธ ๋‹จ์ ˆ์„ ๊ฐ€๋ฅธ๋‹ค โ€” ์‹œ์ž‘ ์‹œ๊ฐ์„ ์ง€ํ‚จ๋‹ค', () => {
44
+ /*
45
+ * ๋งค๋ฒˆ ์ƒˆ๋กœ ์ ์œผ๋ฉด ํ™”๋ฉด์ด ๋Š˜ ใ€Œ๋ฐฉ๊ธˆ ์‹คํŒจํ–ˆ๋‹คใ€๋งŒ ๋ณด๊ฒŒ ๋˜๊ณ , ๊ณ„์†๋œ ๋‹จ์ ˆ์„ ๋Œ€์ˆ˜๋กญ์ง€ ์•Š๊ฒŒ ๋งํ•œ๋‹ค.
46
+ */
47
+ const l = newIngestLedger()
48
+ recordReadFailure(l, 'ETIMEDOUT', T0)
49
+ recordReadFailure(l, 'ETIMEDOUT', T0 + 600_000)
50
+ recordReadFailure(l, 'ETIMEDOUT', T0 + 1_200_000)
51
+ assert.equal(l.readFailure?.consecutive, 3)
52
+ assert.equal(l.readFailure?.sinceMs, T0, '์ฒ˜์Œ ์‹คํŒจํ•œ ์‹œ๊ฐ์„ ์ง€ํ‚จ๋‹ค')
53
+ assert.equal(l.readFailure?.lastAtMs, T0 + 1_200_000, '๋งˆ์ง€๋ง‰ ์‹คํŒจ๋Š” ๊ฐฑ์‹ ๋œ๋‹ค')
54
+ })
55
+
56
+ test('๋นˆ ์ฝ๊ธฐ๋Š” ์„ฑ๊ณต์ด๋‹ค โ€” ์กฐ์šฉํ•œ ์›๋ณธ์„ ๋Š๊ธด ์›๋ณธ์œผ๋กœ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค', () => {
57
+ /*
58
+ * โ˜… ์ด ๋‹จ์ •์ด ๊ณ ์น˜๋ ค๋˜ ๊ฒƒ์˜ **๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ์œผ๋กœ ํ‹€๋ฆฌ๋Š” ๊ฒƒ**์„ ๋ง‰๋Š”๋‹ค. ์›๋ณธ์ด ใ€Œ์ƒˆ ๊ฒƒ์ด ์—†๋‹คใ€๊ณ  ๋‹ตํ•œ ๊ฒƒ์€
59
+ * ๋‹ฟ์•˜๋‹ค๋Š” ๋œป์ด๋‹ค.
60
+ */
61
+ const l = newIngestLedger()
62
+ recordReadFailure(l, 'ETIMEDOUT', T0)
63
+ clearReadFailure(l)
64
+ assert.equal(l.readFailure, undefined, '์ฝ์—ˆ์œผ๋ฉด ๋‹จ์ ˆ ๊ธฐ๋ก์ด ์‚ฌ๋ผ์ง„๋‹ค')
65
+ /* ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๋’ค ๋‹ค์‹œ ์‹คํŒจํ•˜๋ฉด **์ƒˆ ๋‹จ์ ˆ**์ด๋‹ค โ€” ์˜› ์‹œ์ž‘ ์‹œ๊ฐ์„ ๋ฌผ๋ ค๋ฐ›์ง€ ์•Š๋Š”๋‹ค. */
66
+ recordReadFailure(l, 'ETIMEDOUT', T0 + 3_600_000)
67
+ assert.equal(l.readFailure?.consecutive, 1, '์—ฐ์†์ด ๋Š๊ฒผ์œผ๋ฏ€๋กœ 1 ๋ถ€ํ„ฐ')
68
+ assert.equal(l.readFailure?.sinceMs, T0 + 3_600_000, '์ƒˆ ๋‹จ์ ˆ์˜ ์‹œ์ž‘์ด๋‹ค')
69
+ })
70
+
71
+ test('์กฐํšŒ ๋ทฐ๋กœ ๋‚˜๊ฐ„๋‹ค โ€” ์ƒํƒœ์—๋งŒ ์žˆ์œผ๋ฉด ํ™”๋ฉด์ด ๋ชป ๋ณธ๋‹ค', () => {
72
+ const l = newIngestLedger()
73
+ recordReadFailure(l, 'connect ETIMEDOUT', T0, 'inventory')
74
+ const v = ingestHealth(l, 'attached', undefined, T0 + 60_000)
75
+ assert.ok(v.readFailure, '๋ทฐ์— ์‹ค๋ ค์•ผ ํ•œ๋‹ค')
76
+ assert.equal(v.readFailure?.consecutive, 1)
77
+ assert.equal(v.readFailure?.since, new Date(T0).toISOString())
78
+ assert.equal(v.readFailure?.reason, 'connect ETIMEDOUT')
79
+ assert.equal(v.readFailure?.stream, 'inventory')
80
+ })
81
+
82
+ test('๋‹ฟ๊ณ  ์žˆ์œผ๋ฉด ๊ทธ ํ•„๋“œ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค โ€” ๋นˆ ๊ฐ’์ด ใ€Œ๋‹ฟ๋Š”๋‹คใ€๋กœ ์ฝํžˆ์ง€ ์•Š๊ฒŒ', () => {
83
+ const l = newIngestLedger()
84
+ recordIngest(l, 3, [], T0)
85
+ const v = ingestHealth(l, 'attached', undefined, T0 + 1000)
86
+ assert.equal(v.readFailure, undefined)
87
+ /* ๊ฐ€๋“œ๊ฐ€ ์Šค์Šค๋กœ๋ฅผ ์ฆ๋ช…ํ•œ๋‹ค โ€” ์ด ์žฅ๋ถ€๊ฐ€ ์‹ค์ œ๋กœ ๋ฌด์—‡์„ ๋ฐ›์•˜๋‹ค. */
88
+ assert.equal(v.total.offered, 3, '๋นˆ ๋น„๊ต๋กœ ํ†ต๊ณผํ•˜์ง€ ์•Š๋Š”๋‹ค')
89
+ })
90
+
91
+ test('ใ€Œ์œ ์ž… ์—†์Œใ€๊ณผ ใ€Œ๋‹ฟ์ง€ ๋ชปํ•จใ€์€ ๋‹ค๋ฅธ ํŒ์ •์ด๋‹ค', () => {
92
+ /*
93
+ * ์œ ์ž… ์—†์Œ์€ ์›๋ณธ์ด ์กฐ์šฉํ•œ ๊ฒƒ์ผ ์ˆ˜ ์žˆ๊ณ , ๋‹ฟ์ง€ ๋ชปํ•จ์€ ์šฐ๋ฆฌ๊ฐ€ ๋ฌป์ง€๋„ ๋ชปํ•œ ๊ฒƒ์ด๋‹ค. ๋‘ ์ƒํƒœ๊ฐ€ ๊ฐ™์€
94
+ * ๊ฐ’์œผ๋กœ ๋ญ‰๊ฐœ์ง€๋ฉด ํ™”๋ฉด์ด ๋‘ ๋ฌธ์žฅ์„ ๋‹ค๋ฅด๊ฒŒ ๋งํ•  ์ˆ˜ ์—†๋‹ค.
95
+ */
96
+ const quiet = newIngestLedger()
97
+ const dark = newIngestLedger()
98
+ recordReadFailure(dark, 'ETIMEDOUT', T0)
99
+
100
+ const vq = ingestHealth(quiet, 'attached', undefined, T0)
101
+ const vd = ingestHealth(dark, 'attached', undefined, T0)
102
+ assert.equal(vq.lastAt, null, '์กฐ์šฉํ•œ ์ชฝ๋„ ๋ฐ›์€ ๊ฒƒ์ด ์—†๋‹ค')
103
+ assert.equal(vd.lastAt, null, '๋Š๊ธด ์ชฝ๋„ ๋ฐ›์€ ๊ฒƒ์ด ์—†๋‹ค โ€” ๊ทธ ๊ฐ’๋งŒ์œผ๋กœ๋Š” ๊ฐˆ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค')
104
+ assert.notEqual(Boolean(vq.readFailure), Boolean(vd.readFailure), '์ด ์ถ•์ด ๊ทธ ๋‘˜์„ ๊ฐ€๋ฅธ๋‹ค')
105
+ })
106
+
107
+ /*
108
+ * โ”€โ”€ ์ฝ๊ธฐ ์‹คํŒจ๊ฐ€ **ํ”ผ๋“œ๋ฅผ ๋„๋ฉด ์•ˆ ๋œ๋‹ค** (2026-08-23, ๋˜๋Œ๋ฆฐ ์ž๋ฆฌ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
109
+ *
110
+ * ์ฒ˜์Œ ๊ตฌํ˜„์—์„œ ์ฝ๊ธฐ ์‹คํŒจ๊ฐ€ ๋ ˆํผ๋Ÿฐ์Šค ํ–‰์˜ `status` ๋ฅผ `'error'` ๋กœ ์ ์—ˆ๋‹ค. ๊ทธ๊ฒƒ์ด **ํ”ผ๋“œ๋ฅผ ์˜๊ตฌํžˆ
111
+ * ๊ป๋‹ค**: ๋ถ€ํŒ… ์žฌ๋ถ€์ฐฉ์€ `status: 'connected'` ์ธ ๋ ˆํผ๋Ÿฐ์Šค๋งŒ ๋˜์‚ด๋ฆฌ๋ฏ€๋กœ, ์›๋ณธ์ด **์ž ๊น** ๋Š๊ธด ๊ฒƒ์ด
112
+ * ๋‹ค์Œ ์žฌ๊ธฐ๋™์—์„œ ๊ทธ ํŠธ์œˆ์„ ์•„์˜ˆ ๊ตถ๊ฒผ๋‹ค. ์‹ค์ธก์œผ๋กœ ๊ทธ๋ ‡๊ฒŒ ๋‚ฌ๋‹ค โ€” ๊ทธ ํŒ์˜ ๋กœ๊ทธ์— `[twin-live]` ์ค„์ด
113
+ * ํ•œ ์ค„๋„ ์—†์—ˆ๋‹ค.
114
+ *
115
+ * ๋‘ ์ถ•์„ ํ•œ ์นธ์— ์„ž์€ ๊ฒƒ์ด ์›์ธ์ด๋‹ค.
116
+ *
117
+ * status **์—ฐ๊ฒฐ ์„ค์ •์ด ์„ฑ๋ฆฝํ•˜๋‚˜** โ€” ์ ‘์† ์‹œํ—˜ยท๋งˆ์Šคํ„ฐ ๋™๊ธฐ๊ฐ€ ์ •ํ•œ๋‹ค. ์žฌ๋ถ€์ฐฉ์˜ **์ž๊ฒฉ**์ด๋‹ค
118
+ * ์ฝ๊ธฐ ์‹คํŒจ **์ง€๊ธˆ ๋‹ฟ๋‚˜** โ€” ์˜ค๊ฐˆ ์ˆ˜ ์žˆ๋Š” ์‚ฌ์‹ค์ด๊ณ  ์œ ์ž… ์žฅ๋ถ€๊ฐ€ ๊ทธ ์ถ•์„ ๊ฐ–๋Š”๋‹ค
119
+ *
120
+ * ใ€Œ์‚ฌ์‹ค์„ ๋งํ•˜๊ฒŒใ€ ํ•˜๋ ค๋˜ ๊ฒƒ์ด ใ€Œ์‚ฌ์‹ค์„ ๋ชป ๋“ฃ๊ฒŒใ€ ๋งŒ๋“ค์—ˆ๋‹ค. ๊ทธ ๋ฐฉํ–ฅ์˜ ์‹คํŒจ๋Š” ์กฐ์šฉํ•˜๋‹ค โ€” ํ™”๋ฉด์€
121
+ * ใ€Œ์›๋ณธ์— ๋‹ฟ์ง€ ๋ชปํ•œ๋‹คใ€๋ฅผ ์ •์งํ•˜๊ฒŒ ๋งํ•˜๋ฉด์„œ, ์ •์ž‘ ๋‹ค์‹œ ๋‹ฟ์„ ๊ธฐํšŒ๋ฅผ ์žƒ๋Š”๋‹ค.
122
+ */
123
+ test('์ฝ๊ธฐ ์‹คํŒจ๊ฐ€ ๋ ˆํผ๋Ÿฐ์Šค์˜ status ๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€” ๊ทธ๊ฒƒ์ด ์žฌ๋ถ€์ฐฉ์˜ ์ž๊ฒฉ์ด๋‹ค', () => {
124
+ const src = readFileSync(join(import.meta.dirname, '..', 'server', 'service', 'reference', 'reference-live.ts'), 'utf8')
125
+ const code = src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/.*$/gm, '$1')
126
+
127
+ const at = code.indexOf('onReadFailure:')
128
+ assert.ok(at > 0, 'onReadFailure ๋ฐฐ์„ ์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค')
129
+ const body = code.slice(at, at + 600)
130
+
131
+ assert.doesNotMatch(body, /status:/, '์ฝ๊ธฐ ์‹คํŒจ๋Š” status ๋ฅผ ์ ์ง€ ์•Š๋Š”๋‹ค โ€” ์ ์œผ๋ฉด ๋‹ค์Œ ๋ถ€ํŒ…์ด ๊ทธ ํ”ผ๋“œ๋ฅผ ๋˜์‚ด๋ฆฌ์ง€ ์•Š๋Š”๋‹ค')
132
+ /* ๋Œ€์‹  ์ด์œ ๋Š” ๋‚จ๊ธด๋‹ค โ€” ์‚ฌ๋žŒ์ด ๋ชฉ๋ก์—์„œ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ. */
133
+ assert.match(body, /lastError/, '์ด์œ ๋Š” ๋ ˆํผ๋Ÿฐ์Šค ํ–‰์— ๋‚จ๊ธด๋‹ค')
134
+ /* ๊ทธ๋ฆฌ๊ณ  ใ€Œ์ง€๊ธˆ ๋‹ฟ๋‚˜ใ€๋Š” ์žฅ๋ถ€๊ฐ€ ๋‹ตํ•œ๋‹ค. */
135
+ assert.match(body, /recordIngestReadFailure/, '์œ ์ž… ์žฅ๋ถ€๊ฐ€ ๊ทธ ์ถ•์„ ๋“ ๋‹ค')
136
+ })
137
+
138
+ test('์žฌ๋ถ€์ฐฉ์˜ ์ž๊ฒฉ์€ connected ๋ฟ์ด๋‹ค โ€” ๊ทธ๋ž˜์„œ ๊ทธ ๊ฐ’์„ ํ•จ๋ถ€๋กœ ๋ฐ”๊พธ๋ฉด ์•ˆ ๋œ๋‹ค', () => {
139
+ /*
140
+ * ์ด ๋‹จ์ •์ด ์œ„ ๊ฐ€๋“œ์˜ **์ด์œ **๋ฅผ ๋ชป๋ฐ•๋Š”๋‹ค. ์žฌ๋ถ€์ฐฉ ์กฐ๊ฑด์ด ๋„“์–ด์ง€๋ฉด ์œ„ ๊ทœ์œจ์˜ ๊ทผ๊ฑฐ๊ฐ€ ์•ฝํ•ด์ง€๋ฏ€๋กœ,
141
+ * ๋‘˜์ด ํ•จ๊ป˜ ์›€์ง์—ฌ์•ผ ํ•œ๋‹ค.
142
+ */
143
+ const src = readFileSync(join(import.meta.dirname, '..', 'server', 'service', 'reference', 'reference-live.ts'), 'utf8')
144
+ assert.match(src, /find\(\{ where: \{ status: 'connected' \}/, '๋ถ€ํŒ… ์žฌ๋ถ€์ฐฉ์€ connected ๋งŒ ๋˜์‚ด๋ฆฐ๋‹ค')
145
+ })
@@ -0,0 +1,70 @@
1
+ /*
2
+ * โ˜… ์ปค๋„ฅํ„ฐ์˜ **๊ทผ๊ฑฐ ์ˆ˜์ค€** โ€” ๋ชฉ์œผ๋กœ ํ™•์ธํ•œ ๊ฒƒ์„ ํ™•์ธ์œผ๋กœ ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค.
3
+ *
4
+ * โ”€โ”€ ์™œ ์ด ์ถ•์ด ์ƒ๊ฒผ๋‚˜ (2026-08-23, ํ† ๋Œ€ ๋ฆด๋ฆฌ์ฆˆ ์ค€๋น„) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * ๋“ฑ๋ก๋œ ์ปค๋„ฅํ„ฐ ์—ด๋„ท ์ค‘ ์‹ค ์‹œ์Šคํ…œ์— ๋ถ™์–ด ๋ณธ ๊ฒƒ์€ **ํ•˜๋‚˜**๋‹ค(์ฒซ ์‹ค ์—ฐ๋™). ๋‚˜๋จธ์ง€๋Š” ๋ชฉ์— ๋Œ€๊ณ  ๋งŒ๋“ค์—ˆ๋‹ค.
6
+ * ๊ทธ๋Ÿฐ๋ฐ ํ™”๋ฉด์˜ ์ปค๋„ฅํ„ฐ ๋ชฉ๋ก์— ๊ทธ ๊ตฌ๋ถ„์ด ์—†์–ด์„œ **๋ชฉ์œผ๋กœ ํ™•์ธํ•œ ๊ฒƒ์ด ํ™•์ธ์œผ๋กœ ๋ณด์ธ๋‹ค.**
7
+ *
8
+ * ์ด ์ €์žฅ์†Œ๋Š” ๊ฐ™์€ ๋ชจ์–‘์„ ๋Šฅ๋ ฅ ์ถ•์—์„œ ์ด๋ฏธ ๊ฑฐ์ ˆํ–ˆ๋‹ค โ€” ใ€Œ์„ ์–ธ๋งŒ ์žˆ๋Š” ๋Šฅ๋ ฅ์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฑฐ์ง“๋ง์ด ๋œ๋‹คใ€
9
+ * (ยง`capabilitiesOf`). ๊ทธ ๊ทœ์œจ์„ ๊ทผ๊ฑฐ์—๋„ ์„ธ์šด๋‹ค.
10
+ *
11
+ * โ”€โ”€ ๋ฌด์—‡์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ณ  ๋ฌด์—‡์„ ๋ชป ํ•˜๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12
+ * ๋Šฅ๋ ฅ์€ ๊ตฌํ˜„์ด ์žˆ๋Š”์ง€ ๋ณด์•„ ์„ ์–ธ์„ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค. **๊ทผ๊ฑฐ๋Š” ๋ณผ ์ˆ˜ ์—†๋‹ค** โ€” ์‚ฌ๋žŒ์ด ์•„๋Š” ์‚ฌ์‹ค์ด๋‹ค.
13
+ * ๊ทธ๋ž˜์„œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋Š” ํ•˜๋‚˜๋งŒ ๊ฒ€์ฆํ•œ๋‹ค: **์‹ค ์‹œ์Šคํ…œ์„ ์ฃผ์žฅํ•˜๋ฉด ๋ฌด์—‡์— ๋Œ€๊ณ ์ธ์ง€ ๋งํ•ด์•ผ ํ•œ๋‹ค.**
14
+ * ๊ฐ€๋ฆฌํ‚ค๋Š” ๋Œ€์ƒ์ด ์—†๋Š” ์ฃผ์žฅ์€ ์ฃผ์žฅ์ด ์•„๋‹ˆ๊ณ , ๊ทธ๋Ÿฐ ์ฃผ์žฅ์€ ๋‚ก์•„๋„ ์•„๋ฌด๋„ ์•Œ ์ˆ˜ ์—†๋‹ค.
15
+ */
16
+ import { test } from 'node:test'
17
+ import assert from 'node:assert/strict'
18
+ import { groundingOf, type ReferenceAdapter } from '../server/service/reference/reference-adapter.ts'
19
+
20
+ const adapter = (grounding?: any) => ({ type: 't', grounding } as unknown as ReferenceAdapter)
21
+
22
+ test('์„ ์–ธ์ด ์—†์œผ๋ฉด ใ€Œ๋ชจ๋ฅธ๋‹คใ€๋‹ค โ€” ใ€Œ๋ชฉ์ด๋‹คใ€๊ฐ€ ์•„๋‹ˆ๋‹ค', () => {
23
+ /*
24
+ * ๋‘˜์„ ๊ฐ™๊ฒŒ ๋‘๋ฉด ์•„์ง ์„ ์–ธํ•˜์ง€ ์•Š์€ ์ƒˆ ์ปค๋„ฅํ„ฐ๊ฐ€ ๋ชฉ์œผ๋กœ ๋‚™์ธ์ฐํžŒ๋‹ค โ€” ๊ทธ๊ฒƒ๋„ ์—†๋Š” ์‚ฌ์‹ค์ด๋‹ค.
25
+ * ํ™”๋ฉด์€ `unknown` ์„ ใ€Œ์•„์ง ๋ฐํžˆ์ง€ ์•Š์•˜๋‹คใ€๋กœ ๋งํ•ด์•ผ ํ•œ๋‹ค.
26
+ */
27
+ assert.deepEqual(groundingOf(adapter()), { level: 'unknown' })
28
+ assert.deepEqual(groundingOf(undefined), { level: 'unknown' })
29
+ })
30
+
31
+ test('์‹ค ์‹œ์Šคํ…œ์„ ์ฃผ์žฅํ•˜๋ฉด ๋ฌด์—‡์— ๋Œ€๊ณ ์ธ์ง€ ๋งํ•ด์•ผ ํ•œ๋‹ค โ€” ์•„๋‹ˆ๋ฉด ์ฃผ์žฅ์ด ๋‚ด๋ ค๊ฐ„๋‹ค', () => {
32
+ /* โ˜… ์ด ๊ฐ€๋“œ์˜ ํ•ต์‹ฌ. ๋Œ€์ƒ ์—†๋Š” ์ฃผ์žฅ์€ ๋‚ก์•„๋„ ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค. */
33
+ for (const level of ['instance', 'product'] as const) {
34
+ const g = groundingOf(adapter({ level }))
35
+ assert.equal(g.level, 'unknown', `${level} ์ฃผ์žฅ์ด ๋Œ€์ƒ ์—†์ด ํ†ต๊ณผํ•˜๋ฉด ์•ˆ ๋œ๋‹ค`)
36
+ assert.match(g.downgraded ?? '', /does not say what it was verified against/)
37
+ assert.match(g.downgraded ?? '', new RegExp(level), '๋ฌด์—‡์„ ์ฃผ์žฅํ–ˆ๋Š”์ง€ ํ•จ๊ป˜ ๋งํ•œ๋‹ค')
38
+ }
39
+ /* ์—ฌ๋ฐฑ๋งŒ ์žˆ๋Š” ๊ฒƒ๋„ ๋น„์–ด ์žˆ๋Š” ๊ฒƒ์ด๋‹ค โ€” ํ˜•์‹์„ ์ฑ„์›Œ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•˜๊ฒŒ. */
40
+ assert.equal(groundingOf(adapter({ level: 'instance', verifiedAgainst: ' ' })).level, 'unknown')
41
+ })
42
+
43
+ test('๋Œ€์ƒ์„ ๋งํ•˜๋ฉด ๊ทธ ์ฃผ์žฅ์ด ์„ ๋‹ค', () => {
44
+ const g = groundingOf(adapter({ level: 'instance', verifiedAgainst: 'chef v1 ยท jejoup / pocheon' }))
45
+ assert.equal(g.level, 'instance')
46
+ assert.equal(g.verifiedAgainst, 'chef v1 ยท jejoup / pocheon')
47
+ assert.equal(g.downgraded, undefined)
48
+ })
49
+
50
+ test('๋ชฉ๊ณผ ๊ฐ€์ƒ์€ ๋Œ€์ƒ ์—†์ด๋„ ์„ ๋‹ค โ€” ๊ทธ ์ž์ฒด๊ฐ€ ์‚ฌ์‹ค์ด๋‹ค', () => {
51
+ /*
52
+ * `mock` ์€ ใ€Œ์‹ค ์‹œ์Šคํ…œ์— ๋ถ™์–ด ๋ณธ ์ ์ด ์—†๋‹คใ€๋Š” ์‚ฌ์‹ค์ด๊ณ  ๊ทธ๊ฒƒ์„ ๋งํ•˜๋Š” ๋ฐ ๋Œ€์ƒ์ด ํ•„์š”ํ•˜์ง€ ์•Š๋‹ค.
53
+ * `synthetic` ์€ ํ•œ๊ณ„๊ฐ€ ์•„๋‹ˆ๋ผ **์„ฑ์งˆ**์ด๋‹ค โ€” ๊ฐ€์ƒ ์›๋ณธ์€ ์Šค์Šค๋กœ ๋งŒ๋“ค์–ด ๋‚ธ๋‹ค.
54
+ */
55
+ assert.equal(groundingOf(adapter({ level: 'mock' })).level, 'mock')
56
+ assert.equal(groundingOf(adapter({ level: 'synthetic' })).level, 'synthetic')
57
+ })
58
+
59
+ test('instance ์™€ product ๋ฅผ ๊ฐ€๋ฅธ๋‹ค โ€” ๊ทธ ๊ตฌ๋ถ„์ด ์ด ์–ดํœ˜์˜ ์š”์ ์ด๋‹ค', () => {
60
+ /*
61
+ * ์ฒซ ์‹ค ์—ฐ๋™์—์„œ ๊ทธ ๋ฐฐํฌ๋งŒ์˜ ์„ฑ์งˆ์ด ๋„ท ๋“œ๋Ÿฌ๋‚ฌ๋Š”๋ฐ(๊ด€๊ณ„ ๋ฆฌ์†”๋ฒ„๊ฐ€ ๋‹ค๋ฅธ ํ…Œ๋„ŒํŠธ์˜ ํ–‰์„ ๋‹ตํ•œ๋‹ค ยท ๋ชจ๋ฅด๋Š”
62
+ * ํ•„ํ„ฐ๋ฅผ ์กฐ์šฉํžˆ ๋ฌด์‹œํ•œ๋‹ค ยท ์ง€์›Œ์ง„ ํ’ˆ๋ชฉ์˜ BOM ์ด ๋‚จ์•„ ์žˆ๋‹ค ยท ์‚ฌ์‹ค์ƒ ๋ฌดํ•œ์„ ๋œปํ•˜๋Š” ํ‘œ์‹) **๊ทธ๊ฒƒ์ด
63
+ * ์ œํ’ˆ์˜ ์„ฑ์งˆ์ธ์ง€ ๊ทธ ์ธ์Šคํ„ด์Šค์˜ ์„ฑ์งˆ์ธ์ง€ ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค.** ๋‘ ๋ฒˆ์งธ ๋ฐฐํฌ๊ฐ€ ๋ถ™๋Š” ๋‚  ๊ฐˆ๋ฆฐ๋‹ค.
64
+ *
65
+ * ๊ทธ๋ž˜์„œ ํ•˜๋‚˜๋กœ ํ™•์ธํ•œ ๊ฒƒ์„ `product` ๋ผ ๋ถ€๋ฅด๋ฉด ์•ˆ ๋˜๊ณ , ์–ดํœ˜๊ฐ€ ๊ทธ ๊ตฌ๋ถ„์„ ๋‹ด๋Š”๋‹ค.
66
+ */
67
+ const one = groundingOf(adapter({ level: 'instance', verifiedAgainst: 'ํ•œ ๋ฐฐํฌ' }))
68
+ const many = groundingOf(adapter({ level: 'product', verifiedAgainst: '์„ธ ๋ฐฐํฌ' }))
69
+ assert.notEqual(one.level, many.level, '๋‘ ์ˆ˜์ค€์ด ๊ฐ™์€ ๊ฐ’์œผ๋กœ ๋ญ‰๊ฐœ์ง€์ง€ ์•Š๋Š”๋‹ค')
70
+ })
@@ -0,0 +1,52 @@
1
+ /*
2
+ * โ˜… ์†Œ์Šค ์‹œํ—˜์ด ํ˜•์ œ ๋ชจ๋“ˆ์„ ๋ถ€๋ฅผ ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค โ€” **`operato-twin` ์˜ ๊ฐ™์€ ๊ทœ์น™์„ ์ด ํŒจํ‚ค์ง€์—๋„ ๋‘”๋‹ค** โ€” **`.js` ๋กœ ์ ํžŒ ์ƒ๋Œ€ ๊ฒฝ๋กœ๋ฅผ `.ts` ๋กœ ๋˜์งš๋Š”๋‹ค.**
3
+ *
4
+ * โ”€โ”€ ๋ฌด์—‡์ด ์„œ๊ณ  ์žˆ์—ˆ๋‚˜ (2026-08-22) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * ์ด ํŒจํ‚ค์ง€์˜ `npm test` ๋Š” `node --test test/*.test.ts` ๋กœ **์†Œ์Šค๋ฅผ ๊ทธ๋Œ€๋กœ ์‹คํ–‰**ํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์„œ๋ฒ„
6
+ * ์†Œ์Šค๋Š” tsc ๋กœ ๋นŒ๋“œ๋˜๋Š” ์ฝ”๋“œ์ด๋ฏ€๋กœ ์ƒ๋Œ€ import ๋ฅผ `'./x.js'` ๋กœ ์ ๋Š”๋‹ค(๊ทธ๊ฒƒ์ด ๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ์—์„œ ๋งž๋Š”
7
+ * ๊ฒฝ๋กœ๋‹ค). ํƒ€์ž…๋งŒ ์ง€์šฐ๋Š” ์‹คํ–‰์€ ๊ทธ `.js` ๋ฅผ `.ts` ๋กœ ๋ฐ”๊ฟ” ์ฃผ์ง€ ์•Š๋Š”๋‹ค.
8
+ *
9
+ * ๊ทธ๋ž˜์„œ **์†Œ์Šค ๋ชจ๋“ˆ์ด ํ˜•์ œ ๋ชจ๋“ˆ์„ ๊ฐ’์œผ๋กœ ์ฐธ์กฐํ•˜๋Š” ์ˆœ๊ฐ„** ๊ทธ ๋ชจ๋“ˆ ๊ทธ๋ž˜ํ”„๋ฅผ ๋ถ€๋ฅด๋Š” ์‹œํ—˜์ด ์ „๋ถ€ ์„ ๋‹ค.
10
+ * ์‹ค์ธก์œผ๋กœ ํ•˜๋ฃจ์— ๋„ค ์ข…๋ฅ˜๊ฐ€ ์ด ๋ฒฝ์— ๊ฑธ๋ ธ๋‹ค: `identity/namespace.js`(์ •์ฒด์„ฑ ์ด๋ฆ„๊ณต๊ฐ„) ยท
11
+ * `engine/log.js`(๋กœ๊ทธ ๋ฌธ) ยท `isa95-coverage.js`(์ ํ•ฉ์„ฑ ํŒ์ •) ยท `journal-count.js`(์ €๋„ ์„ธ๊ธฐ). ๋Š˜ ๊ฐ™์€ ์˜ค๋ฅ˜๋‹ค.
12
+ *
13
+ * Cannot find module 'โ€ฆ/server/identity/namespace.js' imported from 'โ€ฆ/templates/wms-unit-load.ts'
14
+ *
15
+ * โ”€โ”€ ์™œ ์ด ๋ฐฉ์‹์ธ๊ฐ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
16
+ * ๊ณ ๋ฅด๋˜ ๊ธธ์ด ๋‘˜ ์žˆ์—ˆ๋‹ค.
17
+ *
18
+ * โ‘  ๊ทธ ์‹œํ—˜๋“ค์„ **๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ**(`dist-server`)๋กœ ๋Œ๋ฆฐ๋‹ค โ€” ์ด ํŒจํ‚ค์ง€์— ์ „๋ก€๊ฐ€ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์‹œํ—˜๋งˆ๋‹ค
19
+ * ๊ทธ ๋ฐฐ์„ ์„ ์ ์–ด์•ผ ํ•˜๊ณ , ๊ฑธ๋ฆฌ๋Š” ์‹œํ—˜์ด ๋Š˜ ๋•Œ๋งˆ๋‹ค ๊ฐ™์€ ์ผ์„ ๋‹ค์‹œ ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์‚ฐ์ถœ๋ฌผ์ด ๋‚ก์œผ๋ฉด
20
+ * **๊ณ ์นœ ์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ ๊ฒƒ์ด ๋Œ๋ฉด์„œ ์ดˆ๋ก์œผ๋กœ ๋ณด์ธ๋‹ค.**
21
+ * โ‘ก ์ƒ์ˆ˜๋ฅผ ์“ฐ๋Š” ํŒŒ์ผ ์•ˆ์œผ๋กœ ์˜ฎ๊ธด๋‹ค โ€” ๊ฐ’ ์ฐธ์กฐ๊ฐ€ ์‚ฌ๋ผ์ง€์ง€๋งŒ **์ •๋ณธ์ด ํฉ์–ด์ง„๋‹ค.** ๊ทธ๊ฒƒ์ด ์ด ์ƒ์ˆ˜๋ฅผ ํ•œ
22
+ * ์ž๋ฆฌ์— ๋ชจ์€ ์ด์œ ์™€ ์ •๋ฉด์œผ๋กœ ์–ด๊ธ‹๋‚œ๋‹ค.
23
+ *
24
+ * ๋‘˜ ๋‹ค ์ฆ์ƒ์„ ํ”ผํ•˜๋Š” ๊ฒƒ์ด๊ณ , ์›์ธ์€ **ํ•ด์„ ๊ทœ์น™ ํ•˜๋‚˜**๋‹ค. ๊ทธ๋ž˜์„œ ๊ทœ์น™์„ ๊ณ ์นœ๋‹ค: ์ƒ๋Œ€ ๊ฒฝ๋กœ `.js` ๊ฐ€
25
+ * ์—†๊ณ  ๊ฐ™์€ ์ด๋ฆ„์˜ `.ts` ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„ ์“ด๋‹ค. ํ•œ ์ž๋ฆฌ์— ๋‘๋ฏ€๋กœ ๊ฑธ๋ฆฌ๋Š” ์‹œํ—˜์ด ๋Š˜์–ด๋„ ํ•  ์ผ์ด ์—†๋‹ค.
26
+ *
27
+ * โ”€โ”€ ๋ฌด์—‡์„ ํ•˜์ง€ ์•Š๋Š”๊ฐ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
28
+ * **ํŒจํ‚ค์ง€ ์ด๋ฆ„(`@things-factory/โ€ฆ`)์€ ์†๋Œ€์ง€ ์•Š๋Š”๋‹ค.** ๊ทธ๊ฒƒ์€ ์˜์กด ํŒจํ‚ค์ง€์˜ ์‚ฐ์ถœ๋ฌผ์„ ๊ฐ€๋ฆฌํ‚ค๋Š”
29
+ * ๊ฒƒ์ด๊ณ  ์‹ค์ œ๋กœ `.js` ๊ฐ€ ์žˆ๋‹ค. ๋˜์งš๋Š” ๊ฒƒ์€ **์ƒ๋Œ€ ๊ฒฝ๋กœ**๋ฟ์ด๊ณ , ๊ทธ๊ฒƒ๋„ `.ts` ๊ฐ€ ์‹ค์ œ๋กœ ์žˆ์„ ๋•Œ๋งŒ์ด๋‹ค โ€”
30
+ * ์—†์œผ๋ฉด ์›๋ž˜ ์˜ค๋ฅ˜๊ฐ€ ๊ทธ๋Œ€๋กœ ๋‚œ๋‹ค(์—†๋Š” ๊ฒƒ์„ ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค).
31
+ */
32
+ import { registerHooks } from 'node:module'
33
+ import { existsSync } from 'node:fs'
34
+ import { fileURLToPath, pathToFileURL } from 'node:url'
35
+
36
+ registerHooks({
37
+ resolve(specifier, context, nextResolve) {
38
+ /* ์ƒ๋Œ€ ๊ฒฝ๋กœ์˜ `.js` ๋งŒ ๋ณธ๋‹ค โ€” ํŒจํ‚ค์ง€ ์ด๋ฆ„๊ณผ ๊ทธ ๋ฐ–์˜ ํ™•์žฅ์ž๋Š” ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค. */
39
+ if (!specifier.startsWith('.') || !specifier.endsWith('.js')) return nextResolve(specifier, context)
40
+
41
+ try {
42
+ return nextResolve(specifier, context)
43
+ } catch (err) {
44
+ /* ํ•ด์„์ด ์‹คํŒจํ•œ ๊ทธ๋•Œ๋งŒ `.ts` ๋ฅผ ๋ณธ๋‹ค โ€” ์„ฑ๊ณตํ•˜๋Š” ๊ฒฝ๋กœ์˜ ๋œป์„ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. */
45
+ const parent = context?.parentURL
46
+ if (!parent) throw err
47
+ const asTs = new URL(specifier.slice(0, -3) + '.ts', parent)
48
+ if (!existsSync(fileURLToPath(asTs))) throw err
49
+ return { url: pathToFileURL(fileURLToPath(asTs)).href, shortCircuit: true, format: 'module-typescript' }
50
+ }
51
+ }
52
+ })