@things-factory/headless-twin 10.0.6 โ†’ 10.0.7

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 (150) hide show
  1. package/README.md +37 -24
  2. package/dist-server/engine/entity-delta.d.ts +13 -1
  3. package/dist-server/engine/entity-delta.js +130 -16
  4. package/dist-server/engine/entity-delta.js.map +1 -1
  5. package/dist-server/engine/index.d.ts +4 -0
  6. package/dist-server/engine/index.js +4 -0
  7. package/dist-server/engine/index.js.map +1 -1
  8. package/dist-server/engine/kpi-fold.d.ts +20 -3
  9. package/dist-server/engine/kpi-fold.js +21 -4
  10. package/dist-server/engine/kpi-fold.js.map +1 -1
  11. package/dist-server/engine/kpi-query.d.ts +9 -0
  12. package/dist-server/engine/kpi-query.js +118 -19
  13. package/dist-server/engine/kpi-query.js.map +1 -1
  14. package/dist-server/engine/kpi-target.d.ts +141 -0
  15. package/dist-server/engine/kpi-target.js +137 -0
  16. package/dist-server/engine/kpi-target.js.map +1 -0
  17. package/dist-server/engine/live-attentions.d.ts +4 -2
  18. package/dist-server/engine/live-attentions.js +5 -1
  19. package/dist-server/engine/live-attentions.js.map +1 -1
  20. package/dist-server/engine/measured-estimator.d.ts +50 -0
  21. package/dist-server/engine/measured-estimator.js +78 -0
  22. package/dist-server/engine/measured-estimator.js.map +1 -0
  23. package/dist-server/engine/model-basis.d.ts +23 -0
  24. package/dist-server/engine/model-basis.js +100 -0
  25. package/dist-server/engine/model-basis.js.map +1 -0
  26. package/dist-server/engine/oee-accumulator.d.ts +2 -2
  27. package/dist-server/engine/oee-accumulator.js +4 -4
  28. package/dist-server/engine/oee-accumulator.js.map +1 -1
  29. package/dist-server/engine/spec-coverage.d.ts +49 -0
  30. package/dist-server/engine/spec-coverage.js +60 -0
  31. package/dist-server/engine/spec-coverage.js.map +1 -0
  32. package/dist-server/engine/structure-diff.d.ts +25 -0
  33. package/dist-server/engine/structure-diff.js +63 -0
  34. package/dist-server/engine/structure-diff.js.map +1 -0
  35. package/dist-server/engine/travel-estimator.d.ts +57 -0
  36. package/dist-server/engine/travel-estimator.js +120 -0
  37. package/dist-server/engine/travel-estimator.js.map +1 -0
  38. package/dist-server/engine/twin-engine.d.ts +153 -17
  39. package/dist-server/engine/twin-engine.js +473 -81
  40. package/dist-server/engine/twin-engine.js.map +1 -1
  41. package/dist-server/engine/warm-start.d.ts +5 -5
  42. package/dist-server/engine/warm-start.js +4 -4
  43. package/dist-server/engine/warm-start.js.map +1 -1
  44. package/dist-server/service/index.d.ts +4 -2
  45. package/dist-server/service/index.js +21 -14
  46. package/dist-server/service/index.js.map +1 -1
  47. package/dist-server/service/reference/reference-live.js +2 -1
  48. package/dist-server/service/reference/reference-live.js.map +1 -1
  49. package/dist-server/service/reference/reference-master.d.ts +307 -1
  50. package/dist-server/service/reference/reference-master.js +96 -8
  51. package/dist-server/service/reference/reference-master.js.map +1 -1
  52. package/dist-server/service/reference/reference-resolver.js +3 -3
  53. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  54. package/dist-server/service/reference/template-registry.d.ts +1 -1
  55. package/dist-server/service/reference/template-registry.js.map +1 -1
  56. package/dist-server/service/twin-attention/twin-attention-query.js +1 -1
  57. package/dist-server/service/twin-attention/twin-attention-query.js.map +1 -1
  58. package/dist-server/service/twin-control/twin-control-mutation.js +1 -1
  59. package/dist-server/service/twin-control/twin-control-mutation.js.map +1 -1
  60. package/dist-server/service/twin-event/twin-event-keys.d.ts +1 -1
  61. package/dist-server/service/twin-event/twin-event-keys.js +3 -3
  62. package/dist-server/service/twin-event/twin-event-keys.js.map +1 -1
  63. package/dist-server/service/twin-event/twin-event.d.ts +11 -0
  64. package/dist-server/service/twin-event/twin-event.js +6 -1
  65. package/dist-server/service/twin-event/twin-event.js.map +1 -1
  66. package/dist-server/service/twin-forecast/twin-forecast-query.js +50 -7
  67. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  68. package/dist-server/service/twin-instance/twin-instance.js +1 -1
  69. package/dist-server/service/twin-instance/twin-instance.js.map +1 -1
  70. package/dist-server/service/twin-journal/twin-journal-query.d.ts +9 -0
  71. package/dist-server/service/twin-journal/twin-journal-query.js +54 -3
  72. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  73. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.d.ts +1 -1
  74. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +4 -3
  75. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  76. package/dist-server/service/twin-space/twin-space-area.js +1 -1
  77. package/dist-server/service/twin-space/twin-space-area.js.map +1 -1
  78. package/dist-server/service/twin-space/twin-space-resolver.js +3 -3
  79. package/dist-server/service/twin-space/twin-space-resolver.js.map +1 -1
  80. package/dist-server/service/twin-space/twin-space.d.ts +11 -0
  81. package/dist-server/service/twin-space/twin-space.js +5 -0
  82. package/dist-server/service/twin-space/twin-space.js.map +1 -1
  83. package/dist-server/service/twin-structure/index.d.ts +2 -0
  84. package/dist-server/service/twin-structure/index.js +6 -0
  85. package/dist-server/service/twin-structure/index.js.map +1 -0
  86. package/dist-server/service/twin-structure/twin-structure.d.ts +28 -0
  87. package/dist-server/service/twin-structure/twin-structure.js +97 -0
  88. package/dist-server/service/twin-structure/twin-structure.js.map +1 -0
  89. package/dist-server/service/twin-target/index.d.ts +4 -0
  90. package/dist-server/service/twin-target/index.js +8 -0
  91. package/dist-server/service/twin-target/index.js.map +1 -0
  92. package/dist-server/service/twin-target/twin-target-resolver.d.ts +7 -0
  93. package/dist-server/service/twin-target/twin-target-resolver.js +143 -0
  94. package/dist-server/service/twin-target/twin-target-resolver.js.map +1 -0
  95. package/dist-server/service/twin-target/twin-target.d.ts +25 -0
  96. package/dist-server/service/twin-target/twin-target.js +112 -0
  97. package/dist-server/service/twin-target/twin-target.js.map +1 -0
  98. package/dist-server/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +6 -6
  100. package/server/engine/entity-delta.ts +135 -15
  101. package/server/engine/index.ts +4 -0
  102. package/server/engine/kpi-fold.ts +40 -8
  103. package/server/engine/kpi-query.ts +129 -19
  104. package/server/engine/kpi-target.ts +226 -0
  105. package/server/engine/live-attentions.ts +12 -2
  106. package/server/engine/measured-estimator.ts +91 -0
  107. package/server/engine/model-basis.ts +94 -0
  108. package/server/engine/oee-accumulator.ts +5 -5
  109. package/server/engine/spec-coverage.ts +85 -0
  110. package/server/engine/structure-diff.ts +88 -0
  111. package/server/engine/travel-estimator.ts +133 -0
  112. package/server/engine/twin-engine.ts +498 -84
  113. package/server/engine/warm-start.ts +8 -8
  114. package/server/service/index.ts +7 -0
  115. package/server/service/reference/reference-live.ts +2 -1
  116. package/server/service/reference/reference-master.ts +383 -10
  117. package/server/service/reference/reference-resolver.ts +3 -3
  118. package/server/service/reference/template-registry.ts +1 -1
  119. package/server/service/twin-attention/twin-attention-query.ts +1 -1
  120. package/server/service/twin-control/twin-control-mutation.ts +1 -1
  121. package/server/service/twin-event/twin-event-keys.ts +2 -2
  122. package/server/service/twin-event/twin-event.ts +15 -1
  123. package/server/service/twin-forecast/twin-forecast-query.ts +56 -8
  124. package/server/service/twin-instance/twin-instance.ts +1 -1
  125. package/server/service/twin-journal/twin-journal-query.ts +52 -4
  126. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +4 -3
  127. package/server/service/twin-space/twin-space-area.ts +1 -1
  128. package/server/service/twin-space/twin-space-resolver.ts +3 -3
  129. package/server/service/twin-space/twin-space.ts +14 -0
  130. package/server/service/twin-structure/index.ts +3 -0
  131. package/server/service/twin-structure/twin-structure.ts +93 -0
  132. package/server/service/twin-target/index.ts +5 -0
  133. package/server/service/twin-target/twin-target-resolver.ts +124 -0
  134. package/server/service/twin-target/twin-target.ts +97 -0
  135. package/test/capability-mapping.test.ts +5 -5
  136. package/test/duration-estimators.test.ts +144 -0
  137. package/test/entity-delta.test.ts +150 -19
  138. package/test/ingest-bench.test.ts +9 -9
  139. package/test/kpi-fold.test.ts +232 -3
  140. package/test/live-mirror-parity.test.ts +53 -19
  141. package/test/master-to-twin.test.ts +87 -13
  142. package/test/model-basis.test.ts +86 -0
  143. package/test/oee-accumulator.test.ts +9 -9
  144. package/test/scale-twin-bench.test.ts +22 -22
  145. package/test/spec-coverage.test.ts +113 -0
  146. package/test/streamline-e2e.test.ts +10 -10
  147. package/test/structure-revision-db.test.ts +310 -0
  148. package/test/twin-event-keys.test.ts +2 -2
  149. package/test/vocabulary-guard.test.ts +43 -0
  150. package/test/warm-start.test.ts +9 -9
@@ -0,0 +1,226 @@
1
+ /*
2
+ * ์„ฑ๊ณผ ๋ชฉํ‘œ โ€” **์ˆซ์ž๋ฅผ ํŒ๋‹จ์œผ๋กœ ๋ฐ”๊พธ๋Š” ์ธต.**
3
+ *
4
+ * โ”€โ”€ ์™œ ํ•„์š”ํ•œ๊ฐ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * ํ™”๋ฉด๊ณผ AI ๋Š” "์ฒ˜๋ฆฌ๋Ÿ‰ 345๊ฑด ยท ์ž‘์—…์‹œ๊ฐ„ 29์ดˆ" ๋ฅผ ์ •ํ™•ํžˆ ๋งํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ **๊ทธ๊ฒƒ์ด ์ข‹์€์ง€ ๋‚˜์œ์ง€๋Š” ๋งํ•  ์ˆ˜
6
+ * ์—†๋‹ค.** ํŒ๋‹จ ๊ทผ๊ฑฐ๊ฐ€ ์•ž ๊ตฌ๊ฐ„ ๋น„๊ต๋ฟ์ด๋ผ "์–ด์ œ๋ณด๋‹ค ๋‚˜์•„์กŒ๋‹ค" ๊นŒ์ง€๊ฐ€ ํ•œ๊ณ„์ด๊ณ , ์–ด์ œ๋„ ๋‚˜๋นด์œผ๋ฉด ๊ทธ ๋ง์€
7
+ * ์“ธ๋ชจ๊ฐ€ ์—†๋‹ค. ๋ชฉํ‘œ๊ฐ€ ๋ถ™๋Š” ์ˆœ๊ฐ„ ๊ฐ™์€ ์ˆซ์ž๊ฐ€ **์ •๋ณด์—์„œ ํŒ๋‹จ์œผ๋กœ** ๋ฐ”๋€๋‹ค.
8
+ *
9
+ * โ”€โ”€ ์ธต์„ ์„ž์ง€ ์•Š๋Š”๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
10
+ * ํด๋“œ(`kpi-fold`)๋Š” **์‚ฌ์‹ค๋งŒ** ๋‚ธ๋‹ค โ€” ๋ชฉํ‘œ๋ฅผ ๋ชจ๋ฅธ๋‹ค. ๋ชฉํ‘œ๋ฅผ ์•„๋Š” ์ˆœ๊ฐ„ "์‚ฌ์‹ค์„ ์„ธ๋Š” ๊ทœ์น™" ๊ณผ "์ž˜ํ–ˆ๋‚˜
11
+ * ํŒ์ •ํ•˜๋Š” ๊ทœ์น™" ์ด ํ•œ ํ•จ์ˆ˜์— ์„ž์ด๊ณ , ๋ชฉํ‘œ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์‚ฌ์‹ค ๊ณ„์‚ฐ์„ ๊ฑด๋“œ๋ฆฌ๊ฒŒ ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ํŒ์ •์€
12
+ * ์—ฌ๊ธฐ, **์ˆœ์ˆ˜ ํ•จ์ˆ˜**๋กœ ๋”ฐ๋กœ ๋‘”๋‹ค(ํด๋“œ์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ’์‹ธ๋‹ค).
13
+ *
14
+ * โ”€โ”€ ๋ชฉํ‘œ์˜ ์ง‘ (์‚ฌ์šฉ์ž ๊ฒฐ์ • 2026-08-02) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
15
+ * **ํŠธ์œˆ์€ ์šด์˜ ๋ชฉํ‘œ๋ฅผ ๊ฐ–๊ณ , `@things-factory/kpi` ๋Š” ๊ฒฝ์˜ ๋ชฉํ‘œ๋ฅผ ๊ฐ–๋Š”๋‹ค.** ๊ฒฝ๊ณ„:
16
+ *
17
+ * ํŠธ์œˆ(TwinTarget) ์ด ๊ตฌ๊ฐ„ยท์ด ๊ต๋Œ€ยท์ด ๊ณต๊ฐ„์˜ **์ฆ‰์‹œ ํŒ๋‹จ**. ํ˜„์žฅ ๊ด€๋ฆฌ์ž๊ฐ€ ํŠธ์œˆ ํ™”๋ฉด์—์„œ ๊ณ ์นœ๋‹ค.
18
+ * ๊ฐ’์€ ์ €๋„ ํด๋“œ์—์„œ **๊ทธ๋•Œ๊ทธ๋•Œ ๊ณ„์‚ฐ**๋œ๋‹ค(์ €์žฅ๋œ ๊ฐ’์„ ์ฝ์ง€ ์•Š๋Š”๋‹ค).
19
+ * packages/kpi **๊ธฐ๊ฐ„ ํ‰๊ฐ€**(์กฐ์งยท๋ถ„๊ธฐยท์ ์ˆ˜ยท๊ฐ€์ค‘์น˜ยท์•Œ๋ฆผ). ๊ฐ’์€ ์ €์žฅ๋ผ ์žˆ์–ด์•ผ ํ•œ๋‹ค.
20
+ *
21
+ * ์ด ๊ฒฝ๊ณ„๋ฅผ ์ง€ํ‚ค๋Š” ๊ทœ์น™ ํ•˜๋‚˜: **ํŠธ์œˆ์€ ๋ชฉํ‘œ๋ฅผ ๋ณต์ œํ•˜์ง€ ์•Š๋Š”๋‹ค.** ๊ฒฝ์˜ ๋ชฉํ‘œ๊ฐ€ ํ•„์š”ํ•˜๋ฉด ํŠธ์œˆ์˜ ์‚ฌ์‹ค์„
22
+ * ๋‚ด๋ณด๋‚ด ๊ทธ์ชฝ์ด ์†Œ๋น„ํ•˜๊ณ , ๊ทธ์ชฝ ๋ชฉํ‘œ๋ฅผ ์—ฌ๊ธฐ๋กœ ๋ฒ ๊ปด ์˜ค์ง€ ์•Š๋Š”๋‹ค โ€” ๋ฒ ๋ผ๋Š” ์ˆœ๊ฐ„ ๋‘ ๊ฐœ์˜ ์ง„์‹ค์ด ๋œ๋‹ค.
23
+ */
24
+ /*
25
+ * โ”€โ”€ ์šด์˜ ์ž„๊ณ„(์ปค๋„ attention)์™€์˜ ๊ฒฝ๊ณ„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
26
+ *
27
+ * **attention ์€ ๊ฐœ์ฒด ํ•˜๋‚˜์˜ ์ง€๊ธˆ ์ƒํƒœ**๋‹ค โ€” ์•ต์ปค(์„ค๋น„ยท์ž๋ฆฌยท์˜ค๋”)์™€ ํ•  ์ˆ˜ ์žˆ๋Š” ์กฐ์น˜๋ฅผ ๊ฐ–๋Š”๋‹ค.
28
+ * **target(์—ฌ๊ธฐ)์€ ๊ตฌ๊ฐ„์˜ ์ง‘๊ณ„**๋‹ค โ€” ์•ต์ปค๋„ ์กฐ์น˜๋„ ์—†๊ณ  ์ถ”์„ธ๋ฅผ ํŒ๋‹จํ•œ๋‹ค.
29
+ *
30
+ * ๊ทธ๋ž˜์„œ ์ฐฝ ๋‹จ์œ„ ์ง‘๊ณ„ ํŒ์ •์„ attention ์œผ๋กœ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค: "๊ฐ€๋™๋ฅ  ์ €ํ•˜"(์ฐฝ ํ‰๊ท ) attention ์„ ๋งŒ๋“ค๋ฉด
31
+ * `utilization.ratio` ๋ชฉํ‘œ์™€ **๊ฐ™์€ ์‚ฌ์‹ค์„ ๋‘ ์ธต์ด ๋‹ค๋ฅธ ์ž„๊ณ„๋กœ ํŒ์ •**ํ•˜๊ฒŒ ๋œ๋‹ค.
32
+ * (design/map/performance.md ยง5.0-4 โ€” ํ˜„์žฌ ๋‹ค์„ฏ attention ๊ณผ ์ด ์–ดํœ˜๋Š” ๊ฒน์น˜์ง€ ์•Š๋Š”๋‹ค.)
33
+ */
34
+ import type { DurationStats, KpiResult } from './kpi-fold.js'
35
+
36
+ /** ์ธก์ •๋œ ๊ธฐ๋ก์ด ์žˆ์„ ๋•Œ๋งŒ ๊ฐ’์œผ๋กœ ์ธ์ •ํ•œ๋‹ค โ€” ์—†์œผ๋ฉด ๋ชจ๋ฆ„(0 ์ด ์•„๋‹ˆ๋‹ค). */
37
+ const measured = (k: KpiResult, v: number): number | undefined => (k.eventsInWindow > 0 ? v : undefined)
38
+
39
+ /** ๋ถ„์œ„์ˆ˜๋Š” ํ‘œ๋ณธ์ด ์žˆ์„ ๋•Œ๋งŒ ๊ฐ’์ด๋‹ค โ€” `count === 0` ์˜ `p50Ms: 0` ์€ "0 ๋ฐ€๋ฆฌ์ดˆ" ๊ฐ€ ์•„๋‹ˆ๋ผ "์—†์Œ" ์ด๋‹ค. */
40
+ const statOf = (s: DurationStats, pick: (d: DurationStats) => number): number | undefined =>
41
+ s.count > 0 ? pick(s) : undefined
42
+
43
+ /**
44
+ * ์ง€ํ‘œ ์–ดํœ˜ โ€” **์ด๋ฆ„์„ ํ•œ ๊ณณ์—์„œ ์ •ํ•œ๋‹ค.**
45
+ *
46
+ * ๋ชฉํ‘œ์™€ ์‚ฌ์‹ค์ด ์„œ๋กœ ๋‹ค๋ฅธ ์ด๋ฆ„์„ ์“ฐ๋ฉด ๊ทธ ์‚ฌ์ด์— ๋งคํ•‘ ํ‘œ๊ฐ€ ์ƒ๊ธฐ๊ณ , ๊ทธ ํ‘œ๊ฐ€ ๊ณง ๋ฐฉ์–ธ์ด ๋œ๋‹ค. ๊ทธ๋ž˜์„œ
47
+ * ๋ชฉํ‘œ๊ฐ€ ๊ฐ€๋ฆฌํ‚ฌ ์ˆ˜ ์žˆ๋Š” ์ง€ํ‘œ๋Š” ์—ฌ๊ธฐ ์—ด๊ฑฐ๋œ ๊ฒƒ๋ฟ์ด๊ณ , ๊ฐ’์„ ๊บผ๋‚ด๋Š” ๋ฐฉ๋ฒ•๋„ ์—ฌ๊ธฐ ํ•จ๊ป˜ ์žˆ๋‹ค.
48
+ *
49
+ * **`direction` ์€ ์ง€ํ‘œ์˜ ์„ฑ์งˆ์ด์ง€ ์‚ฌ์šฉ์ž์˜ ์„ ํƒ์ด ์•„๋‹ˆ๋‹ค.** ์ฒ˜๋ฆฌ๋Ÿ‰์€ ๋†’์„์ˆ˜๋ก ์ข‹๊ณ  ๋Œ€๊ธฐ์‹œ๊ฐ„์€ ๋‚ฎ์„์ˆ˜๋ก
50
+ * ์ข‹๋‹ค โ€” ์ด๊ฒƒ์„ ์ž…๋ ฅ์œผ๋กœ ๋ฐ›์œผ๋ฉด ๋ฐ˜๋Œ€๋กœ ์ ์€ ๋ชฉํ‘œ๊ฐ€ ๋งŒ๋“ค์–ด์ง€๊ณ , ๊ทธ ๋’ค ํŒ์ •์ด ์ „๋ถ€ ๋’ค์ง‘ํžŒ๋‹ค.
51
+ */
52
+ export const TWIN_METRIC = {
53
+ /* ๊ธฐ๋ก์ด ์—†๋Š” ๊ตฌ๊ฐ„์€ ์ฒ˜๋ฆฌ๋Ÿ‰ 0 ์ด ์•„๋‹ˆ๋ผ **๋ชจ๋ฆ„**์ด๋‹ค โ€” `eventsInWindow` ๊ฐ€ ๊ทธ๊ฒƒ์„ ๊ฐ€๋ฅธ๋‹ค.
54
+ 0 ์œผ๋กœ ํŒ์ •ํ•˜๋ฉด "์ธก์ • ๋ชป ํ•œ ๊ตฌ๊ฐ„" ์ด "์ผ์„ ์•ˆ ํ•œ ๊ตฌ๊ฐ„" ์œผ๋กœ ๋ณด๊ณ ๋œ๋‹ค. */
55
+ 'throughput.tasks': { direction: 'higher', unit: 'count', of: (k: KpiResult) => measured(k, k.throughput.tasks) },
56
+ 'throughput.orders': { direction: 'higher', unit: 'count', of: (k: KpiResult) => measured(k, k.throughput.orders) },
57
+ /* ์†Œ์š”์‹œ๊ฐ„ ๋ถ„์œ„์ˆ˜๋Š” `count === 0` ์ด๋ฉด **0 ์ด ์•„๋‹ˆ๋ผ ์—†๋‹ค.** ์ด๊ฒƒ์„ 0 ์œผ๋กœ ์ฝ์œผ๋ฉด ์‹œ๊ฐ„ ์ง€ํ‘œ๋Š”
58
+ "๋‚ฎ์„์ˆ˜๋ก ์ข‹๋‹ค" ๋ผ์„œ **์ผ์ด ์—†๋˜ ๊ตฌ๊ฐ„์ด ๋ชฉํ‘œ๋ฅผ ์™„๋ฒฝํžˆ ๋‹ฌ์„ฑํ•œ ๊ฒƒ์œผ๋กœ** ๋ณด๊ณ ๋œ๋‹ค โ€” ๋ฏธ๋‹ฌ์„ ๋†“์น˜๋Š”
59
+ ๊ฒƒ๋ณด๋‹ค ๋‚˜์˜๋‹ค(๊ณ ์น  ๊ณณ์ด ์žˆ๋Š”๋ฐ ์ž˜ํ•˜๊ณ  ์žˆ๋‹ค๊ณ  ๋งํ•œ๋‹ค). ํ…Œ์ŠคํŠธ๊ฐ€ ์ด๊ฒƒ์„ ์žก์•˜๋‹ค. */
60
+ 'leadTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.leadTime, d => d.p50Ms) },
61
+ 'leadTime.p90Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.leadTime, d => d.p90Ms) },
62
+ 'workTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.workTime, d => d.p50Ms) },
63
+ 'waitTime.p50Ms': { direction: 'lower', unit: 'ms', of: (k: KpiResult) => statOf(k.waitTime, d => d.p50Ms) },
64
+ /** ์ž์› ์ ์œ  โ€” ๊ฐœ๋ณ„ ์ž์›์ด ์•„๋‹ˆ๋ผ **์ฐฝ ์ „์ฒด์˜ ํ‰๊ท  ๋น„์œจ**(์ž์›๋ณ„์€ ๊ด€์  ์ถ•์˜ ์ผ์ด๋‹ค). */
65
+ 'utilization.ratio': {
66
+ direction: 'higher',
67
+ unit: 'ratio',
68
+ of: (k: KpiResult) =>
69
+ k.utilization.length ? k.utilization.reduce((s, u) => s + u.ratio, 0) / k.utilization.length : undefined
70
+ }
71
+ } as const
72
+
73
+ export type TwinMetricId = keyof typeof TWIN_METRIC
74
+ export const isTwinMetric = (id: string): id is TwinMetricId => id in TWIN_METRIC
75
+
76
+ /**
77
+ * ์ €์žฅ๋œ ๋ชฉํ‘œ ํ–‰์—์„œ **์ด ๋ฒ”์œ„์— ๊ฑธ๋ฆฐ ๋ชฉํ‘œ**๋ฅผ ๊ณ ๋ฅธ๋‹ค โ€” ์ข์€ ์„ ์–ธ์ด ๋„“์€ ์„ ์–ธ์„ ๋ฎ๋Š”๋‹ค.
78
+ *
79
+ * **์™œ ์ˆœ์ˆ˜ ํ•จ์ˆ˜์ธ๊ฐ€**: ์ด ๊ทœ์น™(๊ณต๊ฐ„ ๋ชฉํ‘œ ์œ„์— ์ธ์Šคํ„ด์Šค ๋ชฉํ‘œ๋ฅผ ์–น๋Š”๋‹ค)์€ ์ง€๊ธˆ๊นŒ์ง€ DB ์ ‘๊ทผ ๋’ค์— ์ˆจ์–ด
80
+ * ์žˆ์–ด์„œ **ํ™”๋ฉด์„ ๋ˆŒ๋Ÿฌ ๋ด์•ผ๋งŒ** ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด ํ”„๋กœ์ ํŠธ๋Š” ๋กœ๊ทธ์ธ ๋’ค ํ™”๋ฉด์„ ์ž๋™์œผ๋กœ
81
+ * ๊ฒ€์ฆํ•  ๋ฐฉ๋ฒ•์ด ์—†์–ด, ์‹ค์ œ๋กœ ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์ด "์‹คํ™”๋ฉด ๋ฏธ๊ฒ€์ฆ" ์œผ๋กœ ์Œ“์˜€๋‹ค. ๊ทœ์น™์„ ๋ฐ–์œผ๋กœ ๋นผ๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€
82
+ * ๊ฐ’์‹ธ์ง„๋‹ค โ€” ์กฐํšŒ๋Š” ํ–‰์„ ๊ฐ€์ ธ์˜ค๊ธฐ๋งŒ ํ•˜๊ณ , **ํŒ๋‹จ์€ ์—ฌ๊ธฐ์„œ** ํ•œ๋‹ค.
83
+ */
84
+ /**
85
+ * ์ถ• ๋ชฉํ‘œ๊นŒ์ง€ ๋‹ด์€ ๋ชฉ๋ก โ€” ์ „์ฒด ๋ชฉํ‘œ์™€ **์ถ•์˜ ํ•œ ๊ฐ’**์— ๊ฑธ๋ฆฐ ๋ชฉํ‘œ๋ฅผ ํ•จ๊ป˜ ๋“ ๋‹ค.
86
+ *
87
+ * ์ถ• ๋ชฉํ‘œ๋Š” ์ „์ฒด ๋ชฉํ‘œ๋ฅผ **๋ฎ์ง€ ์•Š๋Š”๋‹ค.** ๋‹ค๋ฅธ ์งˆ๋ฌธ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค: "ํ˜„์žฅ ์ „์ฒด๊ฐ€ 100๊ฑด" ๊ณผ
88
+ * "์šฉ์ ‘ ๊ตฌ์—ญ์ด 30๊ฑด" ์€ ๋™์‹œ์— ์ฐธ์ผ ์ˆ˜ ์žˆ๋‹ค. ๋ฎ๊ฒŒ ๋งŒ๋“ค๋ฉด ์ถ• ํ•˜๋‚˜์— ๋ชฉํ‘œ๋ฅผ ๊ฑฐ๋Š” ์ˆœ๊ฐ„ ์ „์ฒด ํŒ์ •์ด
89
+ * ์‚ฌ๋ผ์ ธ ์‚ฌ์šฉ์ž๊ฐ€ "๋ชฉํ‘œ๋ฅผ ์ง€์› ๋‚˜?" ๋ฅผ ๋ฌป๊ฒŒ ๋œ๋‹ค.
90
+ */
91
+ export interface ScopedTargets {
92
+ /** ์ฐฝ ์ „์ฒด์— ๊ฑธ๋ฆฐ ๋ชฉํ‘œ. */
93
+ overall: TwinTargetSpec[]
94
+ /** ์ถ• โ†’ (๊ทธ ์ถ•์˜ ๊ฐ’ โ†’ ๋ชฉํ‘œ๋“ค). ์š”์ฒญํ•œ ์ถ•๋งŒ ์ฑ„์šด๋‹ค. */
95
+ byAxisKey: Record<string, TwinTargetSpec[]>
96
+ }
97
+
98
+ /**
99
+ * ์ถ•์˜ ๊ฐ ๊ฐ’์— ๋Œ€ํ•ด ๋ชฉํ‘œ ๋Œ€๋น„ ํŒ์ • โ€” **๊ทธ๋ฃน ํ•ญ๋ชฉ์— ๊ทธ๋Œ€๋กœ ๋ถ™์ผ ํ˜•ํƒœ.**
100
+ *
101
+ * ๊ทธ๋ฃน์˜ ์ง€ํ‘œ๋Š” ์ „์ฒด๋ณด๋‹ค ์ข๋‹ค(์ฃผ๋ฌธ ์ˆ˜ยท์ž์› ์ ์œ ๋Š” ์ถ• ๋‹จ์œ„๋กœ ๋œป์ด ๋‹ค๋ฅด๊ฑฐ๋‚˜ ์—†๋‹ค). ์—†๋Š” ์ง€ํ‘œ์— ๋ชฉํ‘œ๊ฐ€
102
+ * ๊ฑธ๋ ค ์žˆ์œผ๋ฉด **ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค** โ€” ์—ฌ๊ธฐ์„œ 0 ์œผ๋กœ ์ฝ์œผ๋ฉด ์‹œ๊ฐ„ ์ง€ํ‘œ๊ฐ€ "์™„๋ฒฝ ๋‹ฌ์„ฑ" ์œผ๋กœ ๋’ค์ง‘ํžŒ๋‹ค
103
+ * (์ „์ฒด ํŒ์ •์—์„œ ์ด๋ฏธ ๊ฒช์€ ๊ฒฐํ•จ์ด๋‹ค).
104
+ */
105
+ export function judgeGroupTargets(
106
+ group: { key: string; tasks: number; leadTime: { count: number; p50Ms: number }; workTime: { count: number; p50Ms: number }; waitTime: { count: number; p50Ms: number } },
107
+ targets: readonly TwinTargetSpec[] | undefined
108
+ ): TargetVerdict[] {
109
+ const value = (metric: TwinMetricId): number | undefined => {
110
+ switch (metric) {
111
+ case 'throughput.tasks':
112
+ return group.tasks
113
+ case 'leadTime.p50Ms':
114
+ return group.leadTime.count > 0 ? group.leadTime.p50Ms : undefined
115
+ case 'workTime.p50Ms':
116
+ return group.workTime.count > 0 ? group.workTime.p50Ms : undefined
117
+ case 'waitTime.p50Ms':
118
+ return group.waitTime.count > 0 ? group.waitTime.p50Ms : undefined
119
+ default:
120
+ return undefined // ์ถ• ๋‹จ์œ„๋กœ ๋œป์ด ์—†๋Š” ์ง€ํ‘œ(์ฃผ๋ฌธ ์ˆ˜ยท์ž์› ์ ์œ ) โ€” ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค
121
+ }
122
+ }
123
+ const out: TargetVerdict[] = []
124
+ for (const t of targets ?? []) {
125
+ const spec = TWIN_METRIC[t.metric]
126
+ const actual = value(t.metric)
127
+ if (!spec || typeof actual !== 'number' || !Number.isFinite(actual) || !Number.isFinite(t.target)) continue
128
+ out.push(verdictOf(t.metric, actual, t.target, spec.direction))
129
+ }
130
+ return out
131
+ }
132
+
133
+ export function mergeTargets(
134
+ rows: readonly TargetRow[],
135
+ scope: { spaceId?: string; instanceIds: readonly string[]; axis?: string }
136
+ ): TwinTargetSpec[] {
137
+ return scopedTargets(rows, scope).overall
138
+ }
139
+
140
+ /** ์ €์žฅ๋œ ๋ชฉํ‘œ ํ–‰ โ€” ํŒ๋‹จ์— ํ•„์š”ํ•œ ๊ฒƒ๋งŒ(์—”ํ‹ฐํ‹ฐ ์ „์ฒด๋ฅผ ์š”๊ตฌํ•˜์ง€ ์•Š๋Š”๋‹ค: ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ’์‹ธ์ง„๋‹ค). */
141
+ export interface TargetRow {
142
+ metric: string
143
+ target: number
144
+ spaceId?: string | null
145
+ instanceId?: string | null
146
+ axis?: string | null
147
+ axisKey?: string | null
148
+ }
149
+
150
+ /** ์ „์ฒด ๋ชฉํ‘œ์™€ ์ถ• ๋ชฉํ‘œ๋ฅผ ํ•จ๊ป˜ ๊ณ ๋ฅธ๋‹ค โ€” ์ข์€ ์„ ์–ธ(์ธ์Šคํ„ด์Šค)์ด ๋„“์€ ์„ ์–ธ(๊ณต๊ฐ„)์„ ๋ฎ๋Š” ๊ทœ์น™์€ ์–‘์ชฝ ๊ฐ™๋‹ค. */
151
+ export function scopedTargets(
152
+ rows: readonly TargetRow[],
153
+ scope: { spaceId?: string; instanceIds: readonly string[]; axis?: string }
154
+ ): ScopedTargets {
155
+ /* ๊ฐ™์€ ์ž๋ฆฌ(์ง€ํ‘œ ๋˜๋Š” ์ถ•๊ฐ’+์ง€ํ‘œ)์— ๊ณต๊ฐ„ยท์ธ์Šคํ„ด์Šค ๋ชฉํ‘œ๊ฐ€ ํ•จ๊ป˜ ์žˆ์œผ๋ฉด ์ธ์Šคํ„ด์Šค๊ฐ€ ์ด๊ธด๋‹ค. */
156
+ const pick = new Map<string, { target: number; fromInstance: boolean; axisKey?: string; metric: TwinMetricId }>()
157
+ for (const r of rows) {
158
+ if (!isTwinMetric(r.metric) || typeof r.target !== 'number' || !Number.isFinite(r.target)) continue
159
+ const inSpace = !!scope.spaceId && r.spaceId === scope.spaceId
160
+ const inInstance = !!r.instanceId && scope.instanceIds.includes(r.instanceId)
161
+ if (!inSpace && !inInstance) continue
162
+ if (r.axis) {
163
+ if (!scope.axis || r.axis !== scope.axis || !r.axisKey) continue // ์ง€๊ธˆ ๋ณด๋Š” ์ถ•๋งŒ
164
+ }
165
+ const slot = `${r.axis ?? ''}\u0000${r.axisKey ?? ''}\u0000${r.metric}`
166
+ const prev = pick.get(slot)
167
+ if (prev?.fromInstance && !inInstance) continue // ์ธ์Šคํ„ด์Šค ์„ ์–ธ์„ ๊ณต๊ฐ„ ์„ ์–ธ์ด ๋ฎ์ง€ ์•Š๋Š”๋‹ค
168
+ pick.set(slot, { target: r.target, fromInstance: inInstance, axisKey: r.axisKey ?? undefined, metric: r.metric })
169
+ }
170
+ const overall: TwinTargetSpec[] = []
171
+ const byAxisKey: Record<string, TwinTargetSpec[]> = {}
172
+ for (const [slot, v] of pick) {
173
+ const spec = { metric: v.metric, target: v.target }
174
+ if (slot.startsWith('\u0000')) overall.push(spec)
175
+ else (byAxisKey[v.axisKey as string] ??= []).push(spec)
176
+ }
177
+ return { overall, byAxisKey }
178
+ }
179
+
180
+ /** ๋ชฉํ‘œ ํ•˜๋‚˜ โ€” ์–ด๋А ์ง€ํ‘œ๋ฅผ, ์–ผ๋งˆ๋กœ. ๋ฐฉํ–ฅ์€ ์ง€ํ‘œ๊ฐ€ ์•ˆ๋‹ค(ยงTWIN_METRIC). */
181
+ export interface TwinTargetSpec {
182
+ metric: TwinMetricId
183
+ target: number
184
+ }
185
+
186
+ export interface TargetVerdict {
187
+ metric: TwinMetricId
188
+ /** ์ด ๊ตฌ๊ฐ„์˜ ์‹ค์ œ ๊ฐ’. */
189
+ actual: number
190
+ target: number
191
+ direction: 'higher' | 'lower'
192
+ /** ๋ชฉํ‘œ๋ฅผ ๋งŒ์กฑํ–ˆ๋‚˜. */
193
+ met: boolean
194
+ /**
195
+ * ๋ชฉํ‘œ ๋Œ€๋น„ ๋น„์œจ โ€” **ํ•ญ์ƒ "1 ์ด์ƒ์ด๋ฉด ์ข‹๋‹ค"** ๋กœ ์ •๊ทœํ™”ํ•œ๋‹ค(๋ฐฉํ–ฅ์ด ๋‹ค๋ฅธ ์ง€ํ‘œ๋ฅผ ํ•œ ํ™”๋ฉด์—์„œ ๋น„๊ตํ•  ์ˆ˜
196
+ * ์žˆ๊ฒŒ). ์ฒ˜๋ฆฌ๋Ÿ‰์€ `actual/target`, ์‹œ๊ฐ„์€ `target/actual`.
197
+ */
198
+ ratio: number
199
+ }
200
+
201
+ /**
202
+ * ๋ชฉํ‘œ ๋Œ€๋น„ ํŒ์ • โ€” **๋ชฉํ‘œ๊ฐ€ ์—†๋Š” ์ง€ํ‘œ๋Š” ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค.**
203
+ *
204
+ * ๊ฐ’์ด ์—†๋Š” ์ง€ํ‘œ(๊ทธ ๊ตฌ๊ฐ„์— ๊ธฐ๋ก์ด ์—†์–ด `undefined`)๋„ ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์—†๋Š” ๊ฒƒ์„ 0 ์œผ๋กœ ๋†“์œผ๋ฉด
205
+ * "๋ชฉํ‘œ ํฌ๊ฒŒ ๋ฏธ๋‹ฌ" ์ด๋ผ๋Š” **์—†๋Š” ์‚ฌ์‹ค**์ด ๋งŒ๋“ค์–ด์ง€๊ณ , ์‚ฌ์šฉ์ž๋Š” ๋ฉˆ์ถ˜ ์  ์—†๋Š” ๋ผ์ธ์„ ๊ณ ์น˜๋Ÿฌ ๊ฐ„๋‹ค.
206
+ * ๋ชฉํ‘œ๋ฅผ 0 ์œผ๋กœ ์ค€ ๊ฒฝ์šฐ๋„ ๋น„์œจ์„ ๋‚ด์ง€ ์•Š๋Š”๋‹ค(0 ์œผ๋กœ ๋‚˜๋ˆ„์ง€ ์•Š๋Š”๋‹ค) โ€” ๋งŒ์กฑ ์—ฌ๋ถ€๋งŒ ๋‹ตํ•œ๋‹ค.
207
+ */
208
+ export function judgeTargets(kpi: KpiResult, targets: readonly TwinTargetSpec[] | undefined): TargetVerdict[] {
209
+ const out: TargetVerdict[] = []
210
+ for (const t of targets ?? []) {
211
+ const spec = TWIN_METRIC[t.metric]
212
+ if (!spec) continue // ๋ชจ๋ฅด๋Š” ์ง€ํ‘œ ์ด๋ฆ„ โ€” ์ง์ž‘ํ•ด ๋งคํ•‘ํ•˜์ง€ ์•Š๋Š”๋‹ค
213
+ const actual = spec.of(kpi)
214
+ if (typeof actual !== 'number' || !Number.isFinite(actual)) continue // ๊ฐ’์ด ์—†๋‹ค โ‰  ๊ฐ’์ด 0
215
+ if (!Number.isFinite(t.target)) continue
216
+ out.push(verdictOf(t.metric, actual, t.target, spec.direction))
217
+ }
218
+ return out
219
+ }
220
+
221
+ /** ํŒ์ • ์‚ฐ์ˆ˜ ํ•œ ๊ณณ โ€” ์ „์ฒด ํŒ์ •๊ณผ ์ถ• ํŒ์ •์ด **๊ฐ™์€ ๊ทœ์น™**์„ ์“ด๋‹ค(๋‘ ๋ฒŒ์ด๋ฉด ๊ฐˆ๋ผ์ง„๋‹ค). */
222
+ function verdictOf(metric: TwinMetricId, actual: number, target: number, direction: 'higher' | 'lower'): TargetVerdict {
223
+ const met = direction === 'higher' ? actual >= target : actual <= target
224
+ const ratio = direction === 'higher' ? (target > 0 ? actual / target : Number.NaN) : actual > 0 ? target / actual : Number.NaN
225
+ return { metric, actual, target, direction, met, ratio }
226
+ }
@@ -6,10 +6,20 @@
6
6
  */
7
7
  import { deriveAttentions } from '@operato/twin-kernel'
8
8
 
9
- export function withLiveAttentions<S extends { movers?: any[]; nodes?: any[]; orders?: any[]; attentions?: any[] }>(state: S): S {
9
+ export function withLiveAttentions<S extends { equipment?: any[]; locations?: any[]; orders?: any[]; tasks?: any[]; attentions?: any[]; nowTime?: string }>(
10
+ state: S
11
+ ): S {
10
12
  if (!state) return state
11
13
  return {
12
14
  ...state,
13
- attentions: deriveAttentions({ movers: state.movers ?? [], nodes: state.nodes ?? [], orders: state.orders ?? [] })
15
+ /*
16
+ * ์ž…๋ ฅ์„ **์‹œ๋ฎฌ ์ชฝ๊ณผ ๋˜‘๊ฐ™์ด** ๋„ฃ๋Š”๋‹ค. ํ•œ์ชฝ๋งŒ ๋น ์ง€๋ฉด ๊ฐ™์€ ๊ณต์žฅ์ด ํ•œ์ชฝ์—์„œ๋งŒ ๋ง‰ํžŒ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด๊ณ ,
17
+ * ๊ทธ ์–ด๊ธ‹๋‚จ์€ ๊ทœ์น™์ด ์•„๋‹ˆ๋ผ ๋ฐฐ์„  ๋•Œ๋ฌธ์ด๋ผ ์•„๋ฌด ๋ฐ์„œ๋„ ์•ˆ ์žกํžŒ๋‹ค(`tasks` ๋ฅผ ๋น ๋œจ๋ ค ์‹ค์ œ๋กœ ๊ทธ๋Ÿด ๋ป”ํ–ˆ๋‹ค).
18
+ */
19
+ attentions: deriveAttentions(
20
+ { equipment: state.equipment ?? [], locations: state.locations ?? [], orders: state.orders ?? [], tasks: state.tasks ?? [] },
21
+ undefined,
22
+ state.nowTime
23
+ )
14
24
  }
15
25
  }
@@ -0,0 +1,91 @@
1
+ /*
2
+ * ์‹ค์ธก ์†Œ์š” ์ถ”์ •๊ธฐ โ€” **์ด๋ ฅ์—์„œ ๋ฐฐ์šด ๊ฐ’์ด ์ƒ์ˆ˜ยท์„ ์–ธ๊ฐ’์„ ์ด๊ธด๋‹ค.**
3
+ *
4
+ * ๋ฐฐ๊ฒฝ(operato-twin design/plans/simulation-spec.md ยง5-4): ์†Œ์š”์‹œ๊ฐ„์„ ๋ช…์„ธ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๊ฒŒ ๋์ง€๋งŒ
5
+ * ๋ช…์„ธ๋Š” ์„ ์–ธ์ด๋‹ค โ€” ๊ทธ ํ˜„์žฅ์—์„œ ์‹ค์ œ๋กœ ์–ผ๋งˆ ๊ฑธ๋ฆฌ๋Š”์ง€๋Š” ์ €๋„์ด ์•ˆ๋‹ค. `gap-analytics` ๋Š” ์ง€ํ‘œ ์ˆ˜์ค€์˜
6
+ * ํŽธํ–ฅ๋งŒ ๋ฐฐ์› ๊ณ (์˜ˆ์ธก๊ฐ’ ๋ณด์ •), **๊ณต์ •๋ณ„ ์†Œ์š”๋ฅผ ์ถ”์ •๊ธฐ๋กœ ๋˜๋จน์ด๋Š” ๊ฒฝ๋กœ๋Š” ์—†์—ˆ๋‹ค.**
7
+ *
8
+ * ์—ฌ๊ธฐ์„œ ํ•˜๋Š” ์ผ: KPI ํด๋“œ๊ฐ€ ์ž‘์—… ์ข…๋ฅ˜๋ณ„๋กœ ๋‚ธ **์ž‘์—…์‹œ๊ฐ„(์ฐฉ์ˆ˜โ†’์™„๋ฃŒ) p50** ์„ ๊ทธ ์ข…๋ฅ˜์˜ ์†Œ์š”๋กœ ์“ด๋‹ค.
9
+ * ๋ฆฌ๋“œํƒ€์ž„(์ƒ์„ฑโ†’์™„๋ฃŒ)์ด ์•„๋‹ˆ๋ผ ์ž‘์—…์‹œ๊ฐ„์„ ์“ฐ๋Š” ์ด์œ : ๋ฆฌ๋“œํƒ€์ž„์—๋Š” **๋Œ€๊ธฐ๊ฐ€ ์„ž์—ฌ ์žˆ๊ณ **, ๋Œ€๊ธฐ๋Š”
10
+ * ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ด ์ž์› ๊ฒฝํ•ฉ์œผ๋กœ ์Šค์Šค๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•˜๋Š” ๊ฐ’์ด๋‹ค. ๋Œ€๊ธฐ๋ฅผ ์†Œ์š”์— ๋„ฃ์œผ๋ฉด ์ด์ค‘ ๊ณ„์‚ฐ์ด ๋œ๋‹ค.
11
+ *
12
+ * **ํ‰๊ท ๋งŒ ๋ฐฐ์šฐ๋ฉด ๋Œ€๊ธฐ์™€ ๋ณ‘๋ชฉ์„ ๊ณผ์†Œํ‰๊ฐ€ํ•œ๋‹ค.** ์†Œ์š”๊ฐ€ ๋Š˜ ์ •ํ™•ํžˆ ๊ฐ™์œผ๋ฉด ์ค„์ด ์ƒ๊ธฐ์ง€ ์•Š๋Š”๋‹ค โ€” ํ˜„์žฅ์˜
13
+ * ์ค„์€ ํ”๋“ค๋ฆผ์—์„œ ๋‚˜์˜จ๋‹ค. ๊ทธ๋ž˜์„œ p50 ๋งŒ์ด ์•„๋‹ˆ๋ผ **๊ด€์ธก๋œ ํผ์ง(p10..p90)** ์„ ํ•จ๊ป˜ ๋„˜๊ธฐ๊ณ , ํ‘œ๋ณธ์€
14
+ * ์ปค๋„์ด ์ž๊ธฐ ๋‚œ์ˆ˜๋กœ ๋ฝ‘๋Š”๋‹ค(fork ๊ฒฐ์ •์„ฑ). ์–‘์ชฝ ๊ผฌ๋ฆฌ๋ฅผ ๋‹ค ๊ด€์ธก์—์„œ ๊ฐ€์ ธ์˜ค๋Š” ์ด์œ : p90 ๋งŒ ์žˆ์œผ๋ฉด
15
+ * ์•„๋ž˜์ชฝ์„ ์œ„์ชฝ์—์„œ ๊ฑฐ์šธ์ฒ˜๋Ÿผ ๋ฒ ๊ปด์•ผ ํ•˜๋Š”๋ฐ ๊ทธ๊ฑด ๋ฐœ๋ช…์ด๋‹ค.
16
+ *
17
+ * ์ •์ง ๊ทœ์œจ:
18
+ * - ํ‘œ๋ณธ์ด ์ ์œผ๋ฉด ์“ฐ์ง€ ์•Š๋Š”๋‹ค(`minSamples`). ํ•œ๋‘ ๊ฑด์˜ p50 ์€ ๊ทธ ํ˜„์žฅ์˜ ๊ฐ’์ด ์•„๋‹ˆ๋‹ค.
19
+ * - 'unknown' ์ถ•์€ ์“ฐ์ง€ ์•Š๋Š”๋‹ค โ€” ์ข…๋ฅ˜๋ฅผ ๋ชจ๋ฅด๋Š” ๊ธฐ๋ก์„ ํŠน์ • ์ข…๋ฅ˜์˜ ๊ทผ๊ฑฐ๋กœ ์‚ผ์„ ์ˆ˜ ์—†๋‹ค.
20
+ * - ํผ์ง์ด ์„ฑ๋ฆฝํ•˜์ง€ ์•Š์œผ๋ฉด(p10 โ‰ฅ p90, ๋˜๋Š” 0) **ํผ์ง ์—†์ด ํ‰๊ท ๋งŒ** ๋„˜๊ธด๋‹ค. ์—†๋Š” ํ”๋“ค๋ฆผ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.
21
+ * - ๋ฌด์—‡์„ ๋ฐฐ์› ๊ณ  ๋ฌด์—‡์„ ํ‘œ๋ณธ ๋ถ€์กฑ์œผ๋กœ ๋ฒ„๋ ธ๋Š”์ง€ ํ•จ๊ป˜ ๋‚ธ๋‹ค(์กฐ์šฉํžˆ ์ผ๋ถ€๋งŒ ๋ฐ˜์˜ํ•˜์ง€ ์•Š๋Š”๋‹ค).
22
+ */
23
+
24
+ /** ์ปค๋„ `DurationEstimate` ์™€ ๊ฐ™์€ ๋ชจ์–‘(์ปค๋„ ํƒ€์ž…์— ์˜์กดํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ๋กœ ๋งž์ถ˜๋‹ค). */
25
+ export interface MeasuredEstimate {
26
+ meanMs: number
27
+ spread?: { distribution: 'triangular'; minMs: number; maxMs: number; modeMs: number }
28
+ }
29
+
30
+ export interface MeasuredEstimatorResult {
31
+ estimator?: {
32
+ estimate(ctx: { kind: string; fromNode: string; toNode: string; resourceKind?: string }): MeasuredEstimate | undefined
33
+ }
34
+ /** ์ž‘์—… ์ข…๋ฅ˜ โ†’ ์‹ค์ธก ์†Œ์š”(ms). ์ถ”์ •๊ธฐ๊ฐ€ ๋‹ตํ•˜๋Š” ์ข…๋ฅ˜๋“ค. */
35
+ learned: Record<string, number>
36
+ /** ํผ์ง๊นŒ์ง€ ๋ฐฐ์šด ์ข…๋ฅ˜ โ€” ํ‰๊ท ๋งŒ ๋ฐฐ์šด ๊ฒƒ๊ณผ ๊ตฌ๋ณ„ํ•œ๋‹ค(๋ณ€๋™ ์—†๋Š” ์†Œ์š”๋Š” ๋Œ€๊ธฐ๋ฅผ ๊ณผ์†Œํ‰๊ฐ€ํ•œ๋‹ค). */
37
+ spreads: Record<string, { minMs: number; maxMs: number }>
38
+ /** ํ‘œ๋ณธ์ด ๋ชจ์ž๋ผ ์“ฐ์ง€ ์•Š์€ ์ข…๋ฅ˜์™€ ๊ทธ ๊ฑด์ˆ˜ โ€” ๋ฐฐ์šฐ์ง€ ๋ชปํ•œ ๊ฒƒ์„ ๋ฐํžŒ๋‹ค. */
39
+ skipped: { kind: string; count: number }[]
40
+ minSamples: number
41
+ }
42
+
43
+ /** ๊ธฐ๋ณธ ํ‘œ๋ณธ ํ•˜ํ•œ โ€” ์ด๋ณด๋‹ค ์ ์œผ๋ฉด ๊ทธ ์ข…๋ฅ˜๋Š” ๋ฐฐ์šฐ์ง€ ์•Š๋Š”๋‹ค(์ž‘์€ ํ‘œ๋ณธ์˜ p50 ์„ ํ˜„์žฅ ๊ฐ’์œผ๋กœ ์Šน๊ฒฉ ๊ธˆ์ง€). */
44
+ export const DEFAULT_MIN_SAMPLES = 5
45
+
46
+ /**
47
+ * ์ž‘์—… ์ข…๋ฅ˜๋ณ„ KPI ๊ทธ๋ฃน์—์„œ ์ถ”์ •๊ธฐ๋ฅผ ๋งŒ๋“ ๋‹ค(์ˆœ์ˆ˜ โ€” DBยท์ปค๋„ ๋ชจ๋ฆ„).
48
+ * `groups` ๋Š” `computeTwinKpi({ groupBy: 'taskKind' })` ์˜ `groups.items` ๋ฅผ ๊ทธ๋Œ€๋กœ ๋„˜๊ธด๋‹ค.
49
+ */
50
+ export function buildMeasuredEstimator(
51
+ groups: { key: string; workTime?: { count: number; p10Ms?: number; p50Ms: number; p90Ms?: number } }[] | undefined,
52
+ opts: { minSamples?: number } = {}
53
+ ): MeasuredEstimatorResult {
54
+ const minSamples = opts.minSamples ?? DEFAULT_MIN_SAMPLES
55
+ const learned: Record<string, number> = {}
56
+ const spreads: Record<string, { minMs: number; maxMs: number }> = {}
57
+ const estimates: Record<string, MeasuredEstimate> = {}
58
+ const skipped: { kind: string; count: number }[] = []
59
+ for (const g of groups ?? []) {
60
+ if (!g?.key || g.key === 'unknown') continue
61
+ const w = g.workTime
62
+ const count = w?.count ?? 0
63
+ const p50 = w?.p50Ms ?? 0
64
+ if (count < minSamples || !(p50 > 0)) { skipped.push({ kind: g.key, count }); continue }
65
+ const mean = Math.round(p50)
66
+ learned[g.key] = mean
67
+ /* ํผ์ง์€ **๊ด€์ธก๋œ ๋‘ ๊ผฌ๋ฆฌ**๋กœ๋งŒ ๋งŒ๋“ ๋‹ค. ํ•œ์ชฝ์ด๋ผ๋„ ์—†๊ฑฐ๋‚˜ ๊ตฌ๊ฐ„์ด ์„ฑ๋ฆฝํ•˜์ง€ ์•Š์œผ๋ฉด ํ‰๊ท ๋งŒ ๋„˜๊ธด๋‹ค. */
68
+ const lo = w?.p10Ms
69
+ const hi = w?.p90Ms
70
+ if (typeof lo === 'number' && typeof hi === 'number' && lo > 0 && hi > lo) {
71
+ const minMs = Math.round(lo)
72
+ const maxMs = Math.round(hi)
73
+ spreads[g.key] = { minMs, maxMs }
74
+ estimates[g.key] = { meanMs: mean, spread: { distribution: 'triangular', minMs, maxMs, modeMs: mean } }
75
+ } else {
76
+ estimates[g.key] = { meanMs: mean }
77
+ }
78
+ }
79
+ if (!Object.keys(learned).length) return { learned, spreads, skipped, minSamples }
80
+ return {
81
+ learned,
82
+ spreads,
83
+ skipped,
84
+ minSamples,
85
+ estimator: {
86
+ estimate(ctx) {
87
+ return estimates[ctx.kind]
88
+ }
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,94 @@
1
+ /*
2
+ * ๋ชจ๋ธ ๊ทผ๊ฑฐ โ€” **๋ฐฐ์šด ๋ชจ๋ธ์—๋งŒ ๋ณด์ •์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋น„๊ต.**
3
+ *
4
+ * ์˜ˆ์ธก ์ถœ๋ ฅ ๋ณด์ •(P3.2 `twinLearnCalibration`)์€ "์šฐ๋ฆฌ ๋ชจ๋ธ์ด ๋Š˜ ์ด๋งŒํผ ์ ๊ฒŒ ๋ณธ๋‹ค" ๋ฅผ ๋ฐฐ์›Œ ๊ฒฐ๊ณผ๋ฅผ
5
+ * ๋ฐ€์–ด ์ค€๋‹ค. ๊ทธ ํ•™์Šต์€ **๊ทธ๋•Œ์˜ ๋ชจ๋ธ**์„ ์ „์ œ๋กœ ํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์†Œ์š”์‹œ๊ฐ„ ์ถ”์ •๊ธฐ(์‹ค์ธกยท๊ฑฐ๋ฆฌ๊ณ„์‚ฐ)์™€ ๊ณต์ •
6
+ * ๋ช…์„ธ๊ฐ€ ๋“ค์–ด์˜ค๋ฉด์„œ ๋ชจ๋ธ ์ž์ฒด๊ฐ€ ๋ฐ”๋€” ์ˆ˜ ์žˆ๊ฒŒ ๋๋‹ค โ€” ๋ชจ๋ธ์ด ์ด๋ฏธ ๊ทธ ํŽธํ–ฅ์„ ์žƒ์—ˆ๋Š”๋ฐ ๋ณด์ •์ด ๋˜ ๋ฐ€์–ด
7
+ * ์ฃผ๋ฉด **์ด์ค‘ ๋ณด์ •**์ด ๋˜์–ด ์ •ํ™•๋„๊ฐ€ ๋‚˜๋น ์ง„๋‹ค. ํ•™์Šต ์‹œ์ ์˜ accAfter ๋„ ๋”๋Š” ์œ ํšจํ•˜์ง€ ์•Š๋‹ค.
8
+ *
9
+ * ๊ทธ๋ž˜์„œ ๋ณด์ • ๊ธฐ๋ก์— **๊ทธ๋•Œ ๋ชจ๋ธ์˜ ๊ทผ๊ฑฐ**๋ฅผ ํ•จ๊ป˜ ๋‚จ๊ธฐ๊ณ , ์ ์šฉ ์ „์— ์ง€๊ธˆ ๊ทผ๊ฑฐ์™€ ๋น„๊ตํ•œ๋‹ค. ๋‹ฌ๋ผ์กŒ์œผ๋ฉด
10
+ * ์ ์šฉํ•˜์ง€ ์•Š๊ณ  **๊ทธ ์‚ฌ์‹ค์„ ์‘๋‹ต์— ์‹ฃ๋Š”๋‹ค**(์กฐ์šฉํžˆ ์‚ฌ๋ผ์ง€๋ฉด ์‚ฌ์šฉ์ž๋Š” ํ•™์Šต์ด ์™œ ์•ˆ ๋“ฃ๋Š”์ง€ ๋ชจ๋ฅธ๋‹ค).
11
+ *
12
+ * ๋น„๊ต๋Š” **์ƒ๋Œ€ ํ—ˆ์šฉ์˜ค์ฐจ**๋กœ ํ•œ๋‹ค(์–‘์žํ™” ์ง€๋ฌธ์ด ์•„๋‹ˆ๋ผ). ์‹ค์ธก p50 ์€ ์ƒˆ ๊ธฐ๋ก์ด ์Œ“์ผ ๋•Œ๋งˆ๋‹ค 1~2์ดˆ์”ฉ
13
+ * ํ”๋“ค๋ฆฌ๋Š”๋ฐ, ๊ฐ’์„ ๋ฒ„ํ‚ท์œผ๋กœ ๋ญ‰๊ฐœ ๋น„๊ตํ•˜๋ฉด ๋ฒ„ํ‚ท ๊ฒฝ๊ณ„์— ๊ฑธ๋ฆฐ ๊ฐ’์ด 1์ดˆ ๋“œ๋ฆฌํ”„ํŠธ๋กœ๋„ ๋‹ฌ๋ผ์ ธ ๋ฒ„๋ฆฐ๋‹ค โ€”
14
+ * "์ž‘์€ ๋“œ๋ฆฌํ”„ํŠธ๋กœ ํ•™์Šต์„ ๋ฌดํšจํ™”ํ•˜์ง€ ์•Š๋Š”๋‹ค" ๋Š” ๋ชฉ์ ์„ ๊ฒฝ๊ณ„์—์„œ ๋ชป ์ง€ํ‚จ๋‹ค. ๋น„์œจ๋กœ ๋น„๊ตํ•˜๋ฉด ์–ด๋””์„œ๋“ 
15
+ * ๊ฐ™์€ ๊ทœ์น™์ด ์„ฑ๋ฆฝํ•œ๋‹ค: 10% ์ด๋‚ด๋Š” ๊ฐ™์€ ๋ชจ๋ธ, 30์ดˆโ†’47์ดˆ(57%)๋Š” ๋‹ค๋ฅธ ๋ชจ๋ธ.
16
+ *
17
+ * ์žฌ๋ฃŒ๋Š” ํ˜ธ์ŠคํŠธ๊ฐ€ ์•„๋Š” ๊ฒƒ๋“ค์ด๋‹ค(์ปค๋„ API ์— ์˜์กดํ•˜์ง€ ์•Š์•„ ๋ฐœํ–‰ ์ƒํƒœ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค):
18
+ * โ‘  ์‹ค์ธก์œผ๋กœ ๋ฐฐ์šด ์ž‘์—… ์ข…๋ฅ˜๋ณ„ ์†Œ์š” โ‘ก ์ž์› ์ข…๋ฅ˜๋ณ„ ์†๋„ โ‘ข ์„ ์–ธ๋œ ๊ณต์ • ์†Œ์š”.
19
+ */
20
+
21
+ /** ๊ฐ™์€ ๋ชจ๋ธ๋กœ ๋ณผ ์ƒ๋Œ€ ํ—ˆ์šฉ์˜ค์ฐจ โ€” ์ด ๋น„์œจ ์•ˆ์˜ ๋ณ€ํ™”๋Š” ๋“œ๋ฆฌํ”„ํŠธ, ๋„˜์œผ๋ฉด ๋‹ค๋ฅธ ๋ชจ๋ธ. */
22
+ export const BASIS_TOLERANCE = 0.1
23
+
24
+ export interface ModelBasis {
25
+ /** ์ž‘์—… ์ข…๋ฅ˜ โ†’ ์‹ค์ธก ์†Œ์š”(ms). `buildMeasuredEstimator().learned`. */
26
+ measured?: Record<string, number>
27
+ /** ์ž์› ์ข…๋ฅ˜ โ†’ ์†๋„(m/s). `buildTravelEstimator().speedsByKind`. */
28
+ speeds?: Record<string, number>
29
+ /** ์„ ์–ธ๋œ ๊ณต์ • ์†Œ์š” โ€” key + ISO 8601 ์›๋ฌธ(๋ฌธ์ž์—ด์€ ๊ทธ๋Œ€๋กœ ๋น„๊ตํ•œ๋‹ค: ์„ ์–ธ์ด ๋ฐ”๋€Œ๋ฉด ๋‹ค๋ฅธ ๋ชจ๋ธ). */
30
+ declared?: { key: string; duration?: string }[]
31
+ }
32
+
33
+ /** ๋œป ์žˆ๋Š” ์ˆ˜๋งŒ ๋‚จ๊ธด๋‹ค โ€” 0ยท์Œ์ˆ˜ยทNaN ์€ ๋ชจ๋ธ์ด ์•„๋‹ˆ๋‹ค. */
34
+ function clean(map?: Record<string, number>): Record<string, number> {
35
+ const out: Record<string, number> = {}
36
+ for (const [k, v] of Object.entries(map ?? {})) if (Number.isFinite(v) && v > 0) out[k] = v
37
+ return out
38
+ }
39
+
40
+ function declaredMap(list?: { key: string; duration?: string }[]): Record<string, string> {
41
+ const out: Record<string, string> = {}
42
+ for (const o of list ?? []) if (o?.key && o.duration) out[o.key] = o.duration
43
+ return out
44
+ }
45
+
46
+ /** ๋‘ ์ˆ˜์น˜ ์ง€๋„๊ฐ€ ์‹ค์งˆ์ ์œผ๋กœ ๋‹ค๋ฅธ๊ฐ€ โ€” ํ‚ค ์ง‘ํ•ฉ์ด ๋‹ค๋ฅด๊ฑฐ๋‚˜, ๊ฐ’์ด ํ—ˆ์šฉ์˜ค์ฐจ๋ฅผ ๋„˜์–ด ๋‹ค๋ฅด๋ฉด ๋‹ค๋ฅด๋‹ค. */
47
+ function numericChanged(a: Record<string, number>, b: Record<string, number>, tol: number): boolean {
48
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)])
49
+ for (const k of keys) {
50
+ const x = a[k]
51
+ const y = b[k]
52
+ if (x === undefined || y === undefined) return true // ์ƒˆ๋กœ ๋ฐฐ์šด ์ข…๋ฅ˜ยท์‚ฌ๋ผ์ง„ ์ข…๋ฅ˜ = ๋‹ค๋ฅธ ๋ชจ๋ธ
53
+ const base = Math.max(Math.abs(x), Math.abs(y))
54
+ if (base > 0 && Math.abs(x - y) / base > tol) return true
55
+ }
56
+ return false
57
+ }
58
+
59
+ /**
60
+ * ๋ชจ๋ธ์ด ๋‹ฌ๋ผ์กŒ๋Š”๊ฐ€ โ€” ๋ณด์ •์„ ์ ์šฉํ•˜๊ธฐ ์ „์˜ ๊ด€๋ฌธ.
61
+ * ํ•œ์ชฝ ๊ทผ๊ฑฐ๋ฅผ ๋ชจ๋ฅด๋ฉด **ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค**(false) โ€” ๋ชจ๋ฅด๋Š” ๊ฒƒ์„ ๊ทผ๊ฑฐ๋กœ ํ•™์Šต์„ ๋Š์ง€ ์•Š๋Š”๋‹ค.
62
+ */
63
+ export function modelBasisChanged(before?: ModelBasis, after?: ModelBasis, tol: number = BASIS_TOLERANCE): boolean {
64
+ if (!before || !after) return false
65
+ if (numericChanged(clean(before.measured), clean(after.measured), tol)) return true
66
+ if (numericChanged(clean(before.speeds), clean(after.speeds), tol)) return true
67
+ const da = declaredMap(before.declared)
68
+ const db = declaredMap(after.declared)
69
+ const keys = new Set([...Object.keys(da), ...Object.keys(db)])
70
+ for (const k of keys) if (da[k] !== db[k]) return true
71
+ return false
72
+ }
73
+
74
+ /**
75
+ * ์‚ฌ๋žŒ์ด ์ฝ๋Š” ๊ทผ๊ฑฐ ์š”์•ฝ โ€” **๋กœ๊ทธยท์ง„๋‹จ์šฉ**(๋น„๊ต์˜ ๊ทผ๊ฑฐ๊ฐ€ ์•„๋‹ˆ๋‹ค).
76
+ * ์ง€๋ฌธ์œผ๋กœ ๊ฐ™์Œ์„ ํŒ์ •ํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋Š” ์œ„ ์ฃผ์„ ์ฐธ์กฐ. ๋‘ ๊ทผ๊ฑฐ๊ฐ€ ์™œ ๋‹ฌ๋ผ์กŒ๋Š”์ง€ ๋ˆˆ์œผ๋กœ ๋ณด๋ ค๋ฉด ์ด๊ฒƒ์„ ์ฐ๋Š”๋‹ค.
77
+ */
78
+ export function describeModelBasis(basis?: ModelBasis): string {
79
+ if (!basis) return 'unknown'
80
+ const seg: string[] = []
81
+ const m = Object.entries(clean(basis.measured))
82
+ .map(([k, v]) => `${k}=${Math.round(v / 1000)}s`)
83
+ .sort()
84
+ if (m.length) seg.push(`measured(${m.join(',')})`)
85
+ const s = Object.entries(clean(basis.speeds))
86
+ .map(([k, v]) => `${k}=${Math.round(v * 10) / 10}m/s`)
87
+ .sort()
88
+ if (s.length) seg.push(`speed(${s.join(',')})`)
89
+ const d = Object.entries(declaredMap(basis.declared))
90
+ .map(([k, v]) => `${k}=${v}`)
91
+ .sort()
92
+ if (d.length) seg.push(`declared(${d.join(',')})`)
93
+ return seg.length ? seg.join(' ') : 'kernel defaults only'
94
+ }
@@ -2,7 +2,7 @@
2
2
  * OEE ์ด๋ฒคํŠธ ๋ˆ„์ ๊ธฐ (์ˆœ์ˆ˜ยท๊ทผ์‚ฌ) โ€” live-๋ชจ๋“œ OEE ์ž…๋ ฅ ์ƒ์‚ฐ์˜ ํ•œ ๊ฒฝ๋กœ(face2-inbound-live ยง1.1, option โ‘ก).
3
3
  *
4
4
  * ๋ฌธ์ œ: OEE ๊ณต์‹(computeOee)์€ sim/live ๊ณต์œ ๋˜๋‚˜, ์ž…๋ ฅ ์นด์šดํ„ฐ(runMs/downMs/โ€ฆ)๋Š” ๋ฏธ๋Ÿฌ Stateยท์ด๋ฒคํŠธ์— ์—†๋‹ค
5
- * (emitMover ๋Š” status/location/motion ๋งŒ ๋ฐฉ์ถœ). โ†’ live ๋Š” ์นด์šดํ„ฐ๋ฅผ ๋ณ„๋„ ์ƒ์‚ฐํ•ด์•ผ ํ•œ๋‹ค.
5
+ * (emitEquipment ๋Š” status/location/motion ๋งŒ ๋ฐฉ์ถœ). โ†’ live ๋Š” ์นด์šดํ„ฐ๋ฅผ ๋ณ„๋„ ์ƒ์‚ฐํ•ด์•ผ ํ•œ๋‹ค.
6
6
  * ์ด ๋ˆ„์ ๊ธฐ: equipment.status ์ „์ด ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ฌด๋ฒ„๋ณ„ ์‹œ๊ฐ„-in-์ƒํƒœ๋ฅผ ๋ˆ„์ ํ•ด ์นด์šดํ„ฐ๋ฅผ ๊ทผ์‚ฌ ์ƒ์‚ฐ.
7
7
  *
8
8
  * ์ •ํ™•: downMs(๊ณ ์žฅ ์‹œ๊ฐ„ = down ๊ตฌ๊ฐ„ ํ•ฉ) ยท quality(good/scrap = quality.output ๋ธํƒ€). ๊ทผ์‚ฌยทํ•œ๊ณ„:
@@ -79,10 +79,10 @@ export class OeeAccumulator {
79
79
 
80
80
  /**
81
81
  * live State ๋ฅผ OEE ๋กœ ๋ณด๊ฐ• โ€” projector ๋ฏธ๋Ÿฌ State(๊ด€์ธก)์˜ ๋ฌด๋ฒ„์— ๋ˆ„์ ๊ธฐ OEE(๊ณ„์‚ฐ)๋ฅผ ์ฑ„์šด๋‹ค.
82
- * live-๋ชจ๋“œ publishEntityData ๊ฐ€ buildEntityDeltas ์ „์— ์ ์šฉ โ†’ mover data(tag) payload ์— oee ํฌํ•จ
82
+ * live-๋ชจ๋“œ publishEntityData ๊ฐ€ buildEntityDeltas ์ „์— ์ ์šฉ โ†’ equipment data(tag) payload ์— oee ํฌํ•จ
83
83
  * (sim ์€ ์ปค๋„์ด ์ฑ„์›€ โ€” ์ด๋กœ์จ live ๋„ ๋™์ผ payload ํ˜•์ƒ). ์ˆœ์ˆ˜(์ž…๋ ฅ ๋ถˆ๋ณ€, ์–•์€ ๋ณต์‚ฌ).
84
84
  */
85
- export function withLiveOee<S extends { movers?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {
86
- if (!state?.movers) return state
87
- return { ...state, movers: state.movers.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }
85
+ export function withLiveOee<S extends { equipment?: any[] }>(state: S, acc: OeeAccumulator, nowMs?: number): S {
86
+ if (!state?.equipment) return state
87
+ return { ...state, equipment: state.equipment.map(m => ({ ...m, oee: acc.oee(m.id, nowMs) })) }
88
88
  }
@@ -0,0 +1,85 @@
1
+ /*
2
+ * ์‹œ๋ฎฌ ๋ช…์„ธ ์ž๊ธฐ๋ณด๊ณ  โ€” **"์ด ์˜ˆ์ธก์œผ๋กœ ๋ฌด์—‡์„ ๋งํ•  ์ˆ˜ ์žˆ๋‚˜" ๋ฅผ ์˜ˆ์ธก๊ณผ ํ•จ๊ป˜ ๋‚ด๋ณด๋‚ธ๋‹ค.**
3
+ *
4
+ * ๋ฐฐ๊ฒฝ(operato-twin design/plans/simulation-spec.md): ๊ณต์ • ์†Œ์š”ยท์ˆ˜์œจ์ด ์ปค๋„ ์†Œ์Šค์˜ ์ƒ์ˆ˜์˜€๋‹ค. ์ด์ œ
5
+ * ๋งˆ์Šคํ„ฐ๊ฐ€ ๋ช…์„ธ๋ฅผ ์‹ค์–ด ์˜ค๋ฉด ์ปค๋„์ด ๊ทธ๊ฒƒ์œผ๋กœ ๊ตด๋ฆฌ๊ณ , `specCoverage()` ๊ฐ€ ๋ฌด์—‡์„ ์„ ์–ธ๊ฐ’์œผ๋กœ ๋ฌด์—‡์„
6
+ * ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ผ๋Š”์ง€ ๋ณด๊ณ ํ•œ๋‹ค. ๊ทธ ๋ณด๊ณ ๋ฅผ **์˜ˆ์ธก ์‘๋‹ต์— ๋ถ™์ด์ง€ ์•Š์œผ๋ฉด** ๋ฐ›๋Š” ์ชฝ์€ ์ƒ์ˆ˜๋กœ ๊ตด๋ฆฐ ์ˆซ์ž์™€
7
+ * ํ˜„์žฅ ๋ช…์„ธ๋กœ ๊ตด๋ฆฐ ์ˆซ์ž๋ฅผ ๊ตฌ๋ณ„ํ•  ์ˆ˜ ์—†๋‹ค โ€” ๋˜‘๊ฐ™์ด "p50 168" ๋กœ ๋ณด์ธ๋‹ค.
8
+ *
9
+ * ์–ธ์–ด ์ค‘๋ฆฝ: ์—ฌ๊ธฐ์„œ๋Š” **์ฝ”๋“œ์™€ ์ˆ˜์น˜๋งŒ** ๋‚ธ๋‹ค(`qualification` + ์นด์šดํŠธ). ์‚ฌ๋žŒ์ด ์ฝ๋Š” ๋ฌธ์žฅ์€ ํ‘œํ˜„
10
+ * ๊ณ„์ธต(ํด๋ผ i18next / AI ๋‹ต๋ณ€)์ด ๋งŒ๋“ ๋‹ค โ€” attention ๊ณผ ๊ฐ™์€ ๊ทœ์œจ.
11
+ */
12
+
13
+ /** ์˜ˆ์ธก ์ž๊ฒฉ โ€” ์„ ์–ธ๋œ ๋ช…์„ธ๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋ฎ๋Š”์ง€์— ๋”ฐ๋ผ ์ด ์˜ˆ์ธก์œผ๋กœ ์ •๋‹นํ•˜๊ฒŒ ๋งํ•  ์ˆ˜ ์žˆ๋Š” ๋ฒ”์œ„๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค. */
14
+ export type ForecastQualification =
15
+ /** ๋ชจ๋“  ์†Œ์š”๊ฐ€ ์ปค๋„ ๊ธฐ๋ณธ ์ƒ์ˆ˜ โ†’ **์ƒ๋Œ€ ๋น„๊ต๋งŒ**("์ž์› 2๋Œ€ ๋” ๋„ฃ์œผ๋ฉด ๋‚˜์•„์ง€๋‚˜"). ์ ˆ๋Œ€ ์‹œ๊ฐ ์ฃผ์žฅ ๋ถˆ๊ฐ€. */
16
+ | 'relative'
17
+ /** ์ผ๋ถ€๋งŒ ์„ ์–ธยท์‹ค์ธก โ†’ ๊ทธ ๊ตฌ๊ฐ„์€ ์ ‘์ง€, ๋‚˜๋จธ์ง€๋Š” ์ƒ์ˆ˜. ํ˜ผํ•ฉ. */
18
+ | 'partial'
19
+ /** ์†Œ์š” ์ „๋ถ€ ์„ ์–ธ โ†’ ์ ˆ๋Œ€ ์˜ˆ์ธก ์‹œ๋„ ๊ฐ€๋Šฅ. ๋‹จ **์ •ํ™•๋„๋Š” ์ด๋ ฅ์ด ํ™•์ธํ•œ๋‹ค**(์„ ์–ธ != ๊ฒ€์ฆ). */
20
+ | 'absolute-capable'
21
+ /**
22
+ * ์†Œ์š” ์ „๋ถ€๊ฐ€ **์ด ํ˜„์žฅ์˜ ์ด๋ ฅ์—์„œ ์˜จ ๊ฐ’**(์ถ”์ •๊ธฐ) โ†’ ๊ฐ€์žฅ ๊ฐ•ํ•œ ๊ทผ๊ฑฐ. ์„ ์–ธ์€ ์‚ฌ๋žŒ์ด ์ ์€ ๊ฐ’์ด๊ณ 
23
+ * ์ด๊ฒƒ์€ ์‹ค์ œ๋กœ ์ผ์–ด๋‚œ ๊ฐ’์ด๋‹ค. ๊ทธ๋ž˜๋„ "์ •ํ™•ํ•˜๋‹ค" ๊ฐ€ ์•„๋‹ˆ๋ผ "๊ด€์ธก์— ์ ‘์ง€๋๋‹ค" ๋Š” ๋œป์ด๋‹ค.
24
+ */
25
+ | 'calibrated'
26
+
27
+ export interface SpecCoverageReport {
28
+ operations: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters: string[] }[]
29
+ /** ์ด๋ ฅยท๊ณ„์‚ฐ์—์„œ ์˜จ ์†Œ์š”(์ถ”์ •๊ธฐ) โ€” ์„ ์–ธ๊ฐ’๋ณด๋‹ค ๊ฐ•ํ•œ ๊ทผ๊ฑฐ. */
30
+ measuredDurations: number
31
+ declaredDurations: number
32
+ defaultDurations: number
33
+ /** ๋ณ€๋™(๋ถ„ํฌ)์„ ๊ฐ€์ง„ ์ž‘์—… ์ข…๋ฅ˜ ์ˆ˜. **0 ์ด๋ฉด ๋ชจ๋“  ์†Œ์š”๊ฐ€ ๊ณ ์ •๊ฐ’**์ด๋ผ ๋Œ€๊ธฐยท๋ณ‘๋ชฉ์ด ๊ณผ์†Œํ‰๊ฐ€๋œ๋‹ค. */
34
+ varyingDurations: number
35
+ qualification: ForecastQualification
36
+ }
37
+
38
+ /**
39
+ * ๋ช…์„ธ ์ž๊ฒฉ ํŒ์ •(์ˆœ์ˆ˜) โ€” ์ปค๋„ ๋ณด๊ณ ๋ฅผ ๋ฐ›์•„ ๋ถ„๋ฅ˜ํ•œ๋‹ค. ์ปค๋„์„ ๋ชจ๋ฅด๋ฏ€๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ์‰ฝ๋‹ค.
40
+ * ๊ด€์ธก๋œ ์ž‘์—…์ด ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด(`operations` ๋นˆ ๋ฐฐ์—ด) ํŒ์ •ํ•  ๊ทผ๊ฑฐ๊ฐ€ ์—†์œผ๋ฏ€๋กœ **undefined**(๋ชจ๋ฅธ๋‹ค).
41
+ */
42
+ export function classifySpecCoverage(cov: {
43
+ operations?: { kind: string; duration: 'measured' | 'declared' | 'default'; variability?: string; parameters?: string[] }[]
44
+ measuredDurations?: number
45
+ declaredDurations?: number
46
+ defaultDurations?: number
47
+ }): SpecCoverageReport | undefined {
48
+ const ops = cov?.operations
49
+ if (!ops?.length) return undefined
50
+ const operations = ops.map(o => ({
51
+ kind: o.kind,
52
+ duration: o.duration,
53
+ ...(o.variability ? { variability: o.variability } : {}),
54
+ parameters: o.parameters ?? []
55
+ }))
56
+ const measuredDurations = cov.measuredDurations ?? operations.filter(o => o.duration === 'measured').length
57
+ const declaredDurations = cov.declaredDurations ?? operations.filter(o => o.duration === 'declared').length
58
+ const defaultDurations = cov.defaultDurations ?? operations.filter(o => o.duration === 'default').length
59
+ const varyingDurations = operations.filter(o => o.variability && o.variability !== 'constant').length
60
+ /* ์‚ฌ๋‹ค๋ฆฌ: ์ „๋ถ€ ์ด๋ ฅ โ†’ calibrated ยท ์ƒ์ˆ˜ ์—†์Œ โ†’ absolute-capable ยท ์ ‘์ง€๊ฐ€ ํ•˜๋‚˜๋„ ์—†์Œ โ†’ relative ยท ๋‚˜๋จธ์ง€ โ†’ partial. */
61
+ const grounded = measuredDurations + declaredDurations
62
+ const qualification: ForecastQualification =
63
+ grounded === 0
64
+ ? 'relative'
65
+ : defaultDurations > 0
66
+ ? 'partial'
67
+ : declaredDurations === 0
68
+ ? 'calibrated'
69
+ : 'absolute-capable'
70
+ return { operations, measuredDurations, declaredDurations, defaultDurations, varyingDurations, qualification }
71
+ }
72
+
73
+ /**
74
+ * ์ปค๋„์—์„œ ์ฝ๋Š”๋‹ค โ€” **์•„์ง ์ด API ๊ฐ€ ์—†๋Š” ์ปค๋„(๋ฐœํ–‰ ์ด์ „)์ด๋ฉด undefined.**
75
+ * ๊ทธ๋•Œ `undefined` ๋Š” "๋ช…์„ธ๊ฐ€ ์—†๋‹ค" ๊ฐ€ ์•„๋‹ˆ๋ผ "๋ชจ๋ฅธ๋‹ค" ๋‹ค. ์†Œ๋น„์ฒ˜๊ฐ€ ๊ทธ ๋‘˜์„ ์„ž์ง€ ์•Š๋„๋ก ์ฃผ์„์„ ๋‚จ๊ธด๋‹ค:
76
+ * ์—†๋Š” ๊ฒƒ์„ "์ „๋ถ€ ๊ธฐ๋ณธ๊ฐ’" ์ด๋ผ๊ณ  ๋‹จ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค(๋‹จ์ •ํ•˜๋ ค๋ฉด ์ปค๋„์ด ๋งํ•ด ์ค˜์•ผ ํ•œ๋‹ค).
77
+ */
78
+ export function readSpecCoverage(kernel: any): SpecCoverageReport | undefined {
79
+ if (typeof kernel?.specCoverage !== 'function') return undefined
80
+ try {
81
+ return classifySpecCoverage(kernel.specCoverage())
82
+ } catch {
83
+ return undefined
84
+ }
85
+ }
@@ -0,0 +1,88 @@
1
+ /*
2
+ * ๊ตฌ์กฐ ์ด๋ ฅ์˜ **์ฐจ์ด** โ€” "2๋ฒˆ์งธ์— ๋„์žฅ ๋ถ€์Šค๊ฐ€ ๋„ท ๋Š˜์—ˆ๋‹ค" ๋ฅผ ๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ.
3
+ *
4
+ * ๋ฆฌ๋น„์ „์€ ๊ทธ ์‹œ์ ˆ ๊ณต์žฅ์„ ํ†ต์งธ๋กœ ๋‹ด๋Š”๋‹ค(์žฌ์ƒํ•˜๋ ค๋ฉด ์ „์ฒด ์œ„์ƒ์ด ํ•„์š”ํ•˜๋‹ค). ๊ทธ๋Ÿฐ๋ฐ ์‚ฌ๋žŒ์ด ๋ณด๊ณ  ์‹ถ์€
5
+ * ๊ฒƒ์€ ํ†ต์งธ๊ฐ€ ์•„๋‹ˆ๋ผ **๋ฌด์—‡์ด ๋‹ฌ๋ผ์กŒ๋‚˜** ๋‹ค โ€” 62KB ์งœ๋ฆฌ ๋ณด๋“œ ๋‘ ๊ฐœ๋ฅผ ๋‚˜๋ž€ํžˆ ๋†“๊ณ  ๋ˆˆ์œผ๋กœ ๋น„๊ตํ•  ์ˆ˜๋Š”
6
+ * ์—†๋‹ค. ๊ทธ ํ•œ ์ค„์„ ์—ฌ๊ธฐ์„œ ๋งŒ๋“ ๋‹ค.
7
+ *
8
+ * ์ˆœ์ˆ˜ ํ•จ์ˆ˜์ธ ์ด์œ : ๊ฐ™์€ ๋ฌธ์žฅ์„ ํ™”๋ฉด๊ณผ AI ๊ฐ€ ํ•จ๊ป˜ ์จ์•ผ ํ•œ๋‹ค. ํ•œ์ชฝ์—์„œ๋งŒ ๊ณ„์‚ฐํ•˜๋ฉด ๋‘˜์ด ๋‹ค๋ฅธ ๋ง์„ ํ•œ๋‹ค.
9
+ */
10
+
11
+ /** ์ข…๋ฅ˜๋ณ„ ์ฆ๊ฐ โ€” "paint-booth +4" ์ฒ˜๋Ÿผ ์ฝํžŒ๋‹ค. */
12
+ export interface StructureCountChange {
13
+ key: string
14
+ before: number
15
+ after: number
16
+ delta: number
17
+ }
18
+
19
+ export interface StructureDiff {
20
+ /** ์ž๋ฆฌ ์ข…๋ฅ˜๋ณ„ ์ฆ๊ฐ(0 ์ธ ๊ฒƒ์€ ๋นผ๊ณ ). */
21
+ locationTypes: StructureCountChange[]
22
+ /** ์„ค๋น„ ์ข…๋ฅ˜๋ณ„ ์ฆ๊ฐ(0 ์ธ ๊ฒƒ์€ ๋นผ๊ณ ). */
23
+ equipmentKinds: StructureCountChange[]
24
+ locationsAdded: number
25
+ locationsRemoved: number
26
+ equipmentAdded: number
27
+ equipmentRemoved: number
28
+ /** ๊ฐœ์ˆ˜๋Š” ๊ฐ™์€๋ฐ ๋‚ด์šฉ์ด ๋ฐ”๋€ ๊ฒฝ์šฐ(์šฉ๋Ÿ‰ ์กฐ์ •ยท์ž๋ฆฌ ์ด๋™ ๋“ฑ)๊ฐ€ ์žˆ๋Š”๊ฐ€. */
29
+ changedInPlace: boolean
30
+ }
31
+
32
+ const countBy = <T>(rows: readonly T[] | undefined, key: (r: T) => string | undefined): Map<string, number> => {
33
+ const out = new Map<string, number>()
34
+ for (const r of rows ?? []) {
35
+ const k = key(r)
36
+ if (k) out.set(k, (out.get(k) ?? 0) + 1)
37
+ }
38
+ return out
39
+ }
40
+
41
+ const idsOf = (rows: readonly { id?: string }[] | undefined): Set<string> => new Set((rows ?? []).map(r => r.id).filter((x): x is string => !!x))
42
+
43
+ function changes(before: Map<string, number>, after: Map<string, number>): StructureCountChange[] {
44
+ const keys = [...new Set([...before.keys(), ...after.keys()])].sort()
45
+ return keys
46
+ .map(key => ({ key, before: before.get(key) ?? 0, after: after.get(key) ?? 0, delta: (after.get(key) ?? 0) - (before.get(key) ?? 0) }))
47
+ .filter(c => c.delta !== 0)
48
+ }
49
+
50
+ /**
51
+ * ๋‘ ๊ตฌ์กฐ์˜ ์ฐจ์ด. `before` ๊ฐ€ ์—†์œผ๋ฉด **์ตœ์ดˆ ๋ฆฌ๋น„์ „**์ด๋ผ ๋น„๊ต ๋Œ€์ƒ์ด ์—†๋‹ค โ€” ์ „๋ถ€ ์ƒˆ๋กœ ์ƒ๊ธด ๊ฒƒ์œผ๋กœ ์„ผ๋‹ค.
52
+ *
53
+ * ์•„์ด๋””๋Š” **์ง‘ํ•ฉ์œผ๋กœ** ๋น„๊ตํ•œ๋‹ค(์ˆ˜๋งŒ ์„ธ๋ฉด ๋„ท์ด ์‚ฌ๋ผ์ง€๊ณ  ๋„ท์ด ์ƒ๊ธด ๊ต์ฒด๋ฅผ "๋ณ€ํ™” ์—†์Œ" ์œผ๋กœ ๋‹ตํ•œ๋‹ค).
54
+ */
55
+ export function diffStructures(before: any | undefined, after: any): StructureDiff {
56
+ const beforeLocations = before?.locations ?? []
57
+ const beforeEquipment = before?.equipment ?? []
58
+ const afterLocations = after?.locations ?? []
59
+ const afterEquipment = after?.equipment ?? []
60
+
61
+ const beforeLocationIds = idsOf(beforeLocations)
62
+ const afterLocationIds = idsOf(afterLocations)
63
+ const beforeEquipmentIds = idsOf(beforeEquipment)
64
+ const afterEquipmentIds = idsOf(afterEquipment)
65
+
66
+ const added = <T>(a: Set<T>, b: Set<T>) => [...b].filter(x => !a.has(x)).length
67
+ const locationsAdded = added(beforeLocationIds, afterLocationIds)
68
+ const locationsRemoved = added(afterLocationIds, beforeLocationIds)
69
+ const equipmentAdded = added(beforeEquipmentIds, afterEquipmentIds)
70
+ const equipmentRemoved = added(afterEquipmentIds, beforeEquipmentIds)
71
+
72
+ /*
73
+ * ์•„์ด๋””๋Š” ๊ทธ๋Œ€๋กœ์ธ๋ฐ ์†์„ฑ์ด ๋ฐ”๋€ ๊ฒฝ์šฐ โ€” ์šฉ๋Ÿ‰์„ ๋Š˜๋ ธ๊ฑฐ๋‚˜ ์„ค๋น„๋ฅผ ์˜ฎ๊ฒผ๋‹ค. ์ง€๋ฌธ์ด ๋‹ฌ๋ผ ์ƒˆ ๋ฆฌ๋น„์ „์ด
74
+ * ์ƒ๊ฒผ๋Š”๋ฐ ์ฆ๊ฐ์ด ์ „๋ถ€ 0 ์ด๋ฉด, ํ™”๋ฉด์ด "๋ฐ”๋€ ๊ฒŒ ์—†๋‹ค" ๊ณ  ๋‹ตํ•˜๊ฒŒ ๋œ๋‹ค. ๊ทธ๋Ÿฐ ์นจ๋ฌต์„ ๋ง‰๋Š”๋‹ค.
75
+ */
76
+ const changedInPlace =
77
+ !locationsAdded && !locationsRemoved && !equipmentAdded && !equipmentRemoved && !!before
78
+
79
+ return {
80
+ locationTypes: changes(countBy(beforeLocations, (n: any) => n.type), countBy(afterLocations, (n: any) => n.type)),
81
+ equipmentKinds: changes(countBy(beforeEquipment, (m: any) => m.kind), countBy(afterEquipment, (m: any) => m.kind)),
82
+ locationsAdded,
83
+ locationsRemoved,
84
+ equipmentAdded,
85
+ equipmentRemoved,
86
+ changedInPlace
87
+ }
88
+ }