dev-strender commited on
Commit
3dfc93b
ยท
verified ยท
1 Parent(s): 80208ae

feat: switch demo to FT + Opus article review (pipeline_exp_261021_ft_hybrid)

Browse files
This view is limited to 50 files because it contains too many changes. ย  See raw diff
Files changed (50) hide show
  1. README.md +7 -12
  2. __pycache__/logbook.cpython-312.pyc +0 -0
  3. __pycache__/runner.cpython-312.pyc +0 -0
  4. app.py +52 -94
  5. assets/pipelines/pipeline_dev_v40.yaml +0 -71
  6. assets/pipelines/pipeline_dev_v44.yaml +0 -6
  7. assets/pipelines/pipeline_exp_261021_ft_hybrid.yaml +66 -0
  8. assets/prompts/prompt_dev_260917_vsplit/step1.txt +0 -85
  9. assets/prompts/prompt_dev_260917_vsplit/step2a.txt +0 -36
  10. assets/prompts/prompt_dev_260917_vsplit/step2b.txt +0 -33
  11. assets/prompts/prompt_exp_261021_ft_hybrid/ft_system.txt +1 -0
  12. assets/prompts/prompt_exp_261021_ft_hybrid/review.txt +41 -0
  13. assets/prompts/prompt_exp_261021_ft_hybrid/review_sp4.txt +38 -0
  14. assets/resources/compound_whitelist.txt +0 -3136
  15. assets/resources/{vocabulary_v2.csv โ†’ vocabulary.csv} +1 -71
  16. runner.py +64 -76
  17. solar_eval/__pycache__/__init__.cpython-312.pyc +0 -0
  18. solar_eval/__pycache__/_version.cpython-312.pyc +0 -0
  19. solar_eval/__pycache__/config.cpython-312.pyc +0 -0
  20. solar_eval/cli/commands/compare.py +160 -57
  21. solar_eval/cli/commands/failure_keys.py +143 -0
  22. solar_eval/cli/commands/run_eval.py +340 -0
  23. solar_eval/cli/commands/run_meta.py +538 -0
  24. solar_eval/cli/commands/run_progress.py +230 -0
  25. solar_eval/cli/commands/run_resume.py +298 -0
  26. solar_eval/cli/commands/runs.py +228 -775
  27. solar_eval/cli/commands/score.py +187 -0
  28. solar_eval/cli/commands/sync.py +5 -4
  29. solar_eval/cli/config.py +143 -8
  30. solar_eval/cli/formatters.py +88 -44
  31. solar_eval/cli/main.py +3 -0
  32. solar_eval/config.py +3 -0
  33. solar_eval/core/__pycache__/__init__.cpython-312.pyc +0 -0
  34. solar_eval/core/__pycache__/dataset_loader.cpython-312.pyc +0 -0
  35. solar_eval/core/__pycache__/pipeline_compose.cpython-312.pyc +0 -0
  36. solar_eval/core/dataset_loader.py +51 -10
  37. solar_eval/core/dataset_prep.py +169 -0
  38. solar_eval/core/fingerprint.py +19 -7
  39. solar_eval/core/pipeline_factory.py +54 -0
  40. solar_eval/core/row_filter.py +288 -0
  41. solar_eval/core/run_records.py +205 -0
  42. solar_eval/core/runner.py +301 -309
  43. solar_eval/core/snapshot/__init__.py +400 -0
  44. solar_eval/core/snapshot/dataset_block.py +179 -0
  45. solar_eval/core/snapshot/environment.py +210 -0
  46. solar_eval/core/snapshot/impl_hash.py +365 -0
  47. solar_eval/core/snapshot/reproducibility.py +149 -0
  48. solar_eval/core/snapshot/resources.py +78 -0
  49. solar_eval/core/task_discovery.py +1 -1
  50. solar_eval/evaluators/base.py +19 -1
README.md CHANGED
@@ -10,19 +10,14 @@ pinned: false
10
 
11
  # ์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ
12
 
13
- Upstage **solar-mini4** ๊ธฐ๋ฐ˜ ๊ต์—ด ํŒŒ์ดํ”„๋ผ์ธ ๋ฐ๋ชจ์ž…๋‹ˆ๋‹ค. ํ‰๊ฐ€ ํ•˜๋„ค์Šค์™€ **๊ฐ™์€ ์—”์ง„ ์ฝ”๋“œ**๋ฅผ
14
- ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค โ€” ๋ฐ๋ชจ๊ฐ€ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒฐ๊ณผ๋Š” ํ‰๊ฐ€ run ๊ณผ ๊ฐ™์€ ๊ฒฝ๋กœ์—์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค.
15
 
16
  ```
17
- [๊ทœ์น™] ์‚ฌ์ „ ์น˜ํ™˜
18
- [LLM] FT ๊ต์—ด (ft:solar-news-correction)
19
- [LLM] ํŒ์ • โ€” FT ๊ฒฐ๊ณผ๋ฅผ keep/revert
20
- [LLM] ๋ฌธ๋งฅ ๊ต์ • ยท ์–ดํœ˜
21
- [LLM] ๋ฌธ๋งฅ ๊ต์ • ยท ๋ฌธ๋ฒ•
22
- [๊ทœ์น™] ์‚ฌ์ „ ์žฌ์ ์šฉ ยท ๋ณตํ•ฉ์–ด ๋ณต์› ยท ์œจ/๋ฅ  ๊ทœ์น™ ยท ํ›„์ฒ˜๋ฆฌ ยท ์กฐ์‚ฌ ๊ต์ • ยท ์˜คํƒ ๋˜๋Œ๋ฆผ
23
  ```
24
 
25
- LLM ํ˜ธ์ถœ 4ํšŒ, ์š”์ฒญ๋‹น ์ค‘์•™ 3.2์ดˆ(๋‹จ๋ฝ ๊ธฐ์ค€). ๊ธด ๊ธฐ์‚ฌ๋Š” ๋ฌธ์„œ ๋‹จ์œ„๋กœ ๋ถ„ํ• ํ•ด ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
26
-
27
- **์‹คํ–‰ ์ „ ํ•„์š”**: Space Settings โ†’ Secrets ์— `UPSTAGE_API_KEY` ๋ฅผ ๋„ฃ์œผ์„ธ์š”
28
- (์—†์œผ๋ฉด ์‚ฌ์ด๋“œ๋ฐ” ์ž…๋ ฅ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค).
 
10
 
11
  # ์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ
12
 
13
+ FT ๊ต์—ด + Opus ๊ธฐ์‚ฌ ๊ฒ€ํ†  ๊ตฌ์„ฑ(`pipeline_exp_261021_ft_hybrid`) ๋ฐ๋ชจ์ž…๋‹ˆ๋‹ค. ํ‰๊ฐ€ ํ•˜๋„ค์Šค์™€ ๊ฐ™์€ ์—”์ง„ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
 
14
 
15
  ```
16
+ [๊ทœ์น™] ์šฉ์–ด ์‚ฌ์ „ ์น˜ํ™˜
17
+ [LLM] FT ๊ต์—ด (ft:solar-news-correction, ๋ฌธ๋‹จ ๋ฌถ์Œ๋ณ„ ๋™์‹œ ํ˜ธ์ถœ)
18
+ [LLM] ๊ธฐ์‚ฌ ์ „์ฒด ๊ฒ€ํ†  1ํšŒ (Claude Opus): FT ์˜ค๊ต์ • ๋˜๋Œ๋ฆผ + ์‚ฌ์‹คยท์ˆซ์žยท๋ฌธ๋งฅ ๊ต์ • ๋ชฉ๋ก
19
+ [๊ทœ์น™] ๋˜๋Œ๋ฆผยท๊ต์ • ์ ์šฉ ยท ๋”ฐ์˜ดํ‘œ ๋ฐฉํ–ฅ ยท ์›๋ฌธ ๋”ฐ์˜ดํ‘œ ๋ณด์กด ยท ์œจ/๋ฅ  ยท ํ›„์ฒ˜๋ฆฌ
 
 
20
  ```
21
 
22
+ **์‹คํ–‰ ์ „ ํ•„์š”**: Space Settings โ†’ Secrets ์— `UPSTAGE_API_KEY` ์™€ `TIMELY_API_KEY` ๋ฅผ ๋„ฃ์œผ์„ธ์š”
23
+ (์—†์œผ๋ฉด ์‚ฌ์ด๋“œ๋ฐ” ์ž…๋ ฅ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค). ๊ธฐ์‚ฌ ๊ฒ€ํ†  ๋‹จ๊ณ„๋Š” Timely ๋ผ์šฐํ„ฐ๋ฅผ ๊ฑฐ์ณ Claude Opus ๋ฅผ ๋ถ€๋ฆ…๋‹ˆ๋‹ค.
 
 
__pycache__/logbook.cpython-312.pyc ADDED
Binary file (4.65 kB). View file
 
__pycache__/runner.cpython-312.pyc ADDED
Binary file (7.71 kB). View file
 
app.py CHANGED
@@ -1,12 +1,9 @@
1
  """์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ (Streamlit / HF Space).
2
 
3
- solar-mini4 ๊ต์—ด ํŒŒ์ดํ”„๋ผ์ธ์„ solar-eval ์—”์ง„ ๊ทธ๋Œ€๋กœ ์‹คํ–‰ํ•œ๋‹ค โ€” FT ๊ต์—ด โ†’
4
- ํŒ์ •ํ˜• step1 โ†’ ๋ฌธ๋งฅ ๊ต์ • ์–ดํœ˜/๋ฌธ๋ฒ• 2์Šคํ… โ†’ ๊ฒฐ์ •๋ก  ๊ทœ์น™ 6์ธต. ๊ตฌ์„ฑ ์ •์˜์™€ ์‹คํ–‰์€
5
- ์ „๋ถ€ runner.py ์ด๊ณ  ์ด ํŒŒ์ผ์€ UI ๋งŒ ๋‹ด๋‹นํ•œ๋‹ค. ์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ณผ ๊ฒฐ๊ณผ๋Š” logbook.py ๊ฐ€
6
- Supabase ์— ๋‚จ๊ธด๋‹ค (์‹œํฌ๋ฆฟ ์—†์œผ๋ฉด ์กฐ์šฉํžˆ ๊ฑด๋„ˆ๋œ€).
7
-
8
- ๋ชจ๋ธ ์…€๋ ‰ํ„ฐ๋Š” ๋‘์ง€ ์•Š๋Š”๋‹ค โ€” ํŒŒ์ดํ”„๋ผ์ธยทํ”„๋กฌํ”„ํŠธยท๋ชจ๋ธ์€ ํ•œ ๋ฌถ์Œ์ด๊ณ , ๋ชจ๋ธ๋งŒ
9
- ๊ฐˆ์•„๋ผ์šฐ๋ฉด ๋‹ค๋ฅธ ๊ตฌ์„ฑ์ด ๋œ๋‹ค (runner.CONFIGS ์ฃผ์„ ์ฐธ์กฐ).
10
  """
11
 
12
  from __future__ import annotations
@@ -24,33 +21,29 @@ PANE_STYLE = (
24
  "padding:14px 16px;line-height:1.9;font-size:1.02rem;min-height:120px"
25
  )
26
 
27
- # ํ•ฉ์„ฑ ์˜ˆ๋ฌธ โ€” ํ‰๊ฐ€์…‹ gold ๋Š” ์“ฐ์ง€ ์•Š๋Š”๋‹ค(์œ ์ถœ ๋ฐฉ์ง€). ๊ฐ ์ธต์˜ ํ‘œ์ ์„ ์ผ๋ถ€๋Ÿฌ ์‹ฌ์–ด ๋‘” ๋ฌธ์žฅ์ด๋‹ค:
28
- # ๋น„๋ฅ /๊ฐฑ์‹ ๋ฅ /์—ฐ์ฒด๋ฅ (์‚ฌ์ „ยท์œจ๋ฅ  ๊ทœ์น™) ยท WSJ์€ยท160ใŽž์œผ๋กœ(์กฐ์‚ฌ ๊ต์ •) ยท โ–ถ ์™€ ๊ณก์„  ๋”ฐ์˜ดํ‘œ(์˜คํƒ ๋˜๋Œ๋ฆผ ๊ฐ€๋“œ).
29
  SAMPLE_TEXT = (
30
  "โ–ถ์ •๋ถ€์˜ ๊ฐ•๋ ฅํ•œ ๋ถ€๋™์‚ฐ ๊ทœ์ œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ˆ˜๋„๊ถŒ ์•„ํŒŒํŠธ ๋งค๋งค ๊ฑฐ๋ž˜ ๋น„๋ฅ ์€ "
31
  "์˜คํžˆ๋ ค ์ฆ๊ฐ€์„ธ๋ฅผ ๋ณด์˜€๋‹ค. ์‹œ์ค‘ ์€ํ–‰์˜ ์ฃผํƒ ๋‹ด๋ณด ๋Œ€์ถœ ๊ธˆ๋ฆฌ๊ฐ€ ๋‚ฎ์•„์ง€๋ฉด์„œ "
32
  "์‹ค์ˆ˜์š”์ž์˜ ๊ตฌ๋งค ์‹ฌ๋ฆฌ๊ฐ€ ๋˜์‚ด์•„๋‚ฌ๊ธฐ ๋•Œ๋ฌธ์ด๋ผ๋Š” ๋ถ„์„์ด ๋‚˜์˜จ๋‹ค. "
33
  "๊ตญํ† ๋ถ€ ๊ด€๊ณ„์ž๋Š” โ€œ๋Œ€์ถœ ๊ฐฑ์‹ ๋ฅ ๊ณผ ์—ฐ์ฒด๋ฅ ์„ ํ•จ๊ป˜ ๋ด์•ผ ํ•œ๋‹คโ€๋ฉฐ โ€œ๋‹น๋ถ„๊ฐ„ ์ด๋Ÿฐ "
34
- "ํ๋ฆ„์ด ์ด์–ด์งˆ ๊ฒƒโ€์ด๋ผ๊ณ  ๋งํ–ˆ๋‹ค. WSJ์€ ํ•œ๊ตญ์˜ ์ฃผํƒ ์‹œ์žฅ์ด ์ˆ˜๋„๊ถŒ ์ค‘์‹ฌ์œผ๋กœ "
35
- "์žฌํŽธ๋˜๊ณ  ์žˆ๋‹ค๊ณ  ๋ณด๋„ํ–ˆ๋‹ค. ์‹ ๊ทœ ๊ด‘์—ญ์ฒ ๋„๋Š” ์ตœ๊ณ  ์‹œ์† 160ใŽž์œผ๋กœ ์„ค๊ณ„๋๋‹ค."
36
  )
37
 
38
- # ์Šคํ… ์ด๋ฆ„ โ†’ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ๋ผ๋ฒจ. ์„ธ ๊ตฌ์„ฑ์˜ ์Šคํ…์„ ๋ชจ๋‘ ๋‹ด๋Š”๋‹ค (๋ชจ๋ฅด๋Š” ์Šคํ…์€ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ).
39
- # basic_correction ์˜ ๋ผ๋ฒจ์ด ๊ตฌ์„ฑ๋งˆ๋‹ค ๋‹ค๋ฅด๋‹ค โ€” v44 ์—์„œ๋Š” ์ƒ์„ฑ์ด ์•„๋‹ˆ๋ผ FT ๊ฒฐ๊ณผ ํŒ์ •์ด๋‹ค.
40
  STEP_LABELS = {
41
  "vocabulary_sub_pre": "์šฉ์–ด ์‚ฌ์ „ ์น˜ํ™˜",
42
- "proofread": "1์ฐจ ๊ต์—ด (๊ต์—ด ์ „์šฉ FT ๋ชจ๋ธ)",
43
- "basic_correction": "๊ธฐ์ดˆ ๊ต์ • / FT ๊ฒฐ๊ณผ ํŒ์ •",
44
- "context_correction": "๋ฌธ๋งฅ ๊ต์ •",
45
- "context_vocab": "๋ฌธ๋งฅ ๊ต์ • ยท ์–ดํœ˜",
46
- "context_grammar": "๋ฌธ๋งฅ ๊ต์ • ยท ๋ฌธ๋ฒ•",
47
- "style_correction": "๋ฌธ์ฒด ๊ต์ •",
48
- "vocabulary_reapply": "์šฉ์–ด ์‚ฌ์ „ ์žฌ์ ์šฉ",
49
- "compound_whitelist_restore": "๋ณตํ•ฉ์–ด ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ๋ณต์›",
50
  "yul_ryul_normalize": "์œจ/๋ฅ  ๊ทœ์น™ ์ ์šฉ",
51
  "post_process": "ํ›„์ฒ˜๋ฆฌ (๋‹จ์œ„ยทํ†ตํ™”ยท์‰ผํ‘œ)",
52
- "josa_fix": "์กฐ์‚ฌ ๊ต์ • (๊ธฐํ˜ธยท์ˆซ์žยท๋‹จ์œ„ ๋’ค)",
53
- "diff_revert": "์˜คํƒ ๋˜๋Œ๋ฆผ (๊ธ€๋ฆฌํ”„ยท๊ตฌ์กฐ๋ฌธ์žยท์‰ผํ‘œ)",
54
  }
55
 
56
 
@@ -76,64 +69,32 @@ def diff_panes(src: str, dst: str) -> tuple[str, str, int]:
76
 
77
 
78
  class ProgressView:
79
- """์Šคํ… ์ง„ํ–‰์„ st.status ์•ˆ์— ๊ทธ๋ฆฐ๋‹ค โ€” ๋‹จ๊ณ„๋ณ„ ์ถœ๋ ฅ์€ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ณ  '์–ด๋””๊นŒ์ง€ ์™”๋‚˜' ๋งŒ.
80
-
81
- ๋ฌธ์„œ ๋‹จ์œ„ chunk ๋ชจ๋“œ์—์„œ๋Š” chunk ๋“ค์ด **๋ณ‘๋ ฌ**๋กœ ๋Œ์•„ ์ด๋ฒคํŠธ๊ฐ€ ์„ž์—ฌ ๋“ค์–ด์˜ค๋ฏ€๋กœ, chunk ๋งˆ๋‹ค ์ค„์„ ํ•˜๋‚˜์”ฉ
82
- ๋‘๊ณ (๋ช‡ ๋‹จ๊ณ„๊นŒ์ง€ ์™”๋‚˜) ์ „์ฒด ์ง„ํ–‰ ๋ฐ”๋Š” (๋๋‚œ ๋‹จ๊ณ„ ์ˆ˜) / (chunk ์ˆ˜ ร— ๋‹จ๊ณ„ ์ˆ˜) ๋กœ ์žก๋Š”๋‹ค.
83
- """
84
 
85
  def __init__(self, names: list[str]) -> None:
86
  self.names = names
87
- self.total_steps = len(names)
88
- self.n_chunks = 1
89
- self.done: dict[int, int] = {1: 0} # chunk ๋ฒˆํ˜ธ โ†’ ๋๋‚œ ๋‹จ๊ณ„ ์ˆ˜
90
- self.current: dict[int, str] = {} # chunk ๋ฒˆํ˜ธ ๏ฟฝ๏ฟฝ ์ง„ํ–‰ ์ค‘ ๋‹จ๊ณ„ ๋ผ๋ฒจ
91
  self.status = st.status("๊ต์—ด ์ค‘์ž…๋‹ˆ๋‹คโ€ฆ", expanded=True)
92
- self.bar = self.status.progress(0.0, text=f"0 / {self.total_steps} ๋‹จ๊ณ„")
93
  self.lines = self.status.empty()
94
  self._render()
95
 
96
- def on_step(self, event: str, index: int, total: int, name: str, doc_chunk=None) -> None:
97
- ci = doc_chunk[0] if doc_chunk else 1
98
- if doc_chunk:
99
- self.n_chunks = doc_chunk[1]
100
- self.done.setdefault(ci, 0)
101
- label = STEP_LABELS.get(name, name)
102
- if event == "start":
103
- self.current[ci] = label
104
- elif event == "chunk":
105
- self.current[ci] = f"{label} ยท ์กฐ๊ฐ {index}/{total}"
106
- elif event == "done":
107
- self.done[ci] = index
108
- self.current.pop(ci, None)
109
- finished = sum(self.done.values())
110
- grand = self.n_chunks * self.total_steps
111
- unit = "๋‹จ๊ณ„" if self.n_chunks == 1 else f"๋‹จ๊ณ„ (๋ฌธ์„œ ์กฐ๊ฐ {self.n_chunks}๊ฐœ ๋ณ‘๋ ฌ)"
112
- self.bar.progress(min(finished / grand, 1.0), text=f"{finished} / {grand} {unit}")
113
  self._render()
114
 
115
  def _render(self) -> None:
116
- if self.n_chunks == 1:
117
- done = self.done.get(1, 0)
118
- rows = []
119
- for i, name in enumerate(self.names, 1):
120
- label = STEP_LABELS.get(name, name)
121
- if i <= done:
122
- rows.append(f"โœ… {label}")
123
- elif i == done + 1:
124
- rows.append(f"โณ **{self.current.get(1, label)}**")
125
- else:
126
- rows.append(f"โ–ซ๏ธ {label}")
127
- else:
128
- rows = []
129
- for ci in range(1, self.n_chunks + 1):
130
- done = self.done.get(ci, 0)
131
- if done >= self.total_steps:
132
- rows.append(f"โœ… ์กฐ๊ฐ {ci}: ์™„๋ฃŒ")
133
- elif ci in self.current:
134
- rows.append(f"โณ ์กฐ๊ฐ {ci}: **{self.current[ci]}** ({done}/{self.total_steps})")
135
- else:
136
- rows.append(f"โ–ซ๏ธ ์กฐ๊ฐ {ci}: ๋Œ€๊ธฐ")
137
  self.lines.markdown(" \n".join(rows))
138
 
139
  def finish(self, elapsed_s: float) -> None:
@@ -144,49 +105,47 @@ class ProgressView:
144
 
145
 
146
  @st.cache_resource(show_spinner="ํŒŒ์ดํ”„๋ผ์ธ ์ค€๋น„ ์ค‘โ€ฆ")
147
- def get_pipeline(label: str):
148
- """๊ตฌ์„ฑ ๋ผ๋ฒจ๋กœ ์บ์‹œํ•œ๋‹ค โ€” ๊ตฌ์„ฑ์ด ๋Š˜๋ฉด ๊ทธ ๊ตฌ์„ฑ์˜ ํŒŒ์ดํ”„๋ผ์ธ์ด ๋”ฐ๋กœ ์กฐ๋ฆฝ๋œ๋‹ค."""
149
- return runner.build_pipeline(label)
150
 
151
 
152
  st.set_page_config(page_title="์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ", page_icon="๐Ÿ“ฐ", layout="wide")
153
  st.title("๐Ÿ“ฐ ์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ")
154
- st.caption("solar-mini4 ํŒŒ์ดํ”„๋ผ์ธ ยท ํ‰๊ฐ€ ํ•˜๋„ค์Šค์™€ ๊ฐ™์€ ์—”์ง„ ์ฝ”๋“œ")
155
 
156
  with st.sidebar:
157
  st.subheader("์„ค์ •")
158
- if not os.environ.get("UPSTAGE_API_KEY"):
159
- key = st.text_input(
160
- "UPSTAGE_API_KEY",
161
- type="password",
162
- help="HF Space ์—์„œ๋Š” Settings โ†’ Secrets ๋กœ ์„ค์ •ํ•˜์„ธ์š”.",
163
- )
164
- if key:
165
- os.environ["UPSTAGE_API_KEY"] = key
166
- label = runner.DEFAULT_LABEL
167
- cfg = runner.BY_LABEL[label]
168
  st.markdown(f"**{cfg.label}**")
169
  st.caption(cfg.note)
170
- st.markdown(f"- ๋ชจ๋ธ `{cfg.model}`\n- ํŒŒ์ดํ”„๋ผ์ธ `{cfg.pipeline}`\n- ํ”„๋กฌํ”„ํŠธ `{cfg.prompt}`")
171
 
172
- text = st.text_area("๊ธฐ์‚ฌ ๋ณธ๋ฌธ", value=SAMPLE_TEXT, height=320, max_chars=4000)
173
 
174
  if st.button("๊ต์—ด ์‹คํ–‰", type="primary", disabled=not text.strip()):
175
- if not os.environ.get("UPSTAGE_API_KEY"):
 
176
  st.error(
177
- "UPSTAGE_API_KEY ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค โ€” ์‚ฌ์ด๋“œ๋ฐ”์— ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ Space Secrets ์— ์„ค์ •ํ•˜์„ธ์š”."
178
  )
179
  st.stop()
180
 
181
  source = text.strip()
182
- pipeline = get_pipeline(label)
183
- # ์ž…๋ ฅ์€ ์‹คํ–‰ ์ „์— ๋‚จ๊ธด๋‹ค โ€” ์‹คํ–‰์ด ์‹คํŒจํ•ด๋„ ๋ฌด์—‡์ด ๋“ค์–ด์™”๋Š”์ง€๋Š” ์•Œ ์ˆ˜ ์žˆ๊ฒŒ
184
  article_id = logbook.save_article(source)
185
 
186
  progress = ProgressView(runner.step_names(pipeline))
187
  try:
188
- result = runner.run_proofread(pipeline, source, label=label, on_step=progress.on_step)
189
- except Exception as e: # noqa: BLE001 โ€” ๋ฐ๋ชจ ํ‘œ๋ฉด์—์„œ๋Š” ์›์ธ ์š”์•ฝ๋งŒ ๋ณด์—ฌ์ค€๋‹ค
190
  progress.fail()
191
  st.error(f"์‹คํ–‰ ์‹คํŒจ: {type(e).__name__}: {e}")
192
  st.stop()
@@ -204,7 +163,6 @@ if st.button("๊ต์—ด ์‹คํ–‰", type="primary", disabled=not text.strip()):
204
  st.caption(f"๊ธฐ๋ก ์‹คํŒจ (๊ฒฐ๊ณผ์—๋Š” ์˜ํ–ฅ ์—†์Œ): {logbook.last_error()}")
205
 
206
  left, right, _n_changed = diff_panes(source, result["output"])
207
-
208
  c1, c2 = st.columns(2, gap="medium")
209
  with c1:
210
  st.subheader("์›๋ฌธ")
 
1
  """์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ (Streamlit / HF Space).
2
 
3
+ FT ๊ต์—ด + Opus ๊ธฐ์‚ฌ ๊ฒ€ํ†  ๊ตฌ์„ฑ์„ solar-eval ์—”์ง„ ๊ทธ๋Œ€๋กœ ์‹คํ–‰ํ•œ๋‹ค - FT ๊ต์—ด ๋ชจ๋ธ์ด ๋ฌธ๋‹จ ๋ฌถ์Œ๋งˆ๋‹ค 1์ฐจ ๊ต์—ดํ•˜๊ณ ,
4
+ Claude Opus ๊ฐ€ ๊ธฐ์‚ฌ ์ „์ฒด๋ฅผ ํ•œ ๋ฒˆ ๋ณด๊ณ  FT ์˜ค๊ต์ •์„ ๋˜๋Œ๋ฆฌ๋ฉฐ ์‚ฌ์‹คยท์ˆซ์žยท๋ฌธ๋งฅ ์˜ค๋ฅ˜๋ฅผ ๊ณ ์นœ ๋’ค ๊ฒฐ์ •๋ก  ๊ทœ์น™์„ ๊ฑด๋‹ค.
5
+ ๊ตฌ์„ฑ ์ •์˜์™€ ์‹คํ–‰์€ ์ „๋ถ€ runner.py ์ด๊ณ  ์ด ํŒŒ์ผ์€ UI ๋งŒ ๋‹ด๋‹นํ•œ๋‹ค. ์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ณผ ๊ฒฐ๊ณผ๋Š” logbook.py ๊ฐ€
6
+ Supabase ์— ๋‚จ๊ธด๋‹ค(์‹œํฌ๋ฆฟ์ด ์—†์œผ๋ฉด ์กฐ์šฉํžˆ ๊ฑด๋„ˆ๋œ€).
 
 
 
7
  """
8
 
9
  from __future__ import annotations
 
21
  "padding:14px 16px;line-height:1.9;font-size:1.02rem;min-height:120px"
22
  )
23
 
24
+ # ํ•ฉ์„ฑ ์˜ˆ๋ฌธ - ํ‰๊ฐ€์…‹ gold ๋Š” ์“ฐ์ง€ ์•Š๋Š”๋‹ค(์œ ์ถœ ๋ฐฉ์ง€). ๊ฐ ์ธต์˜ ํ‘œ์ ์„ ์ผ๋ถ€๋Ÿฌ ์‹ฌ์–ด ๋‘” ๋ฌธ์žฅ์ด๋‹ค:
25
+ # ๋น„๋ฅ /๊ฐฑ์‹ ๋ฅ /์—ฐ์ฒด๋ฅ (์‚ฌ์ „ยท์œจ๋ฅ  ๊ทœ์น™) ยท ์˜๋ฌธํ˜• ์–ด๋ฏธ ยท 160ใŽž์œผ๋กœ(์กฐ์‚ฌ) ยท ๊ณง์€ ๋”ฐ์˜ดํ‘œ(์›๋ฌธ ๋”ฐ์˜ดํ‘œ ๋ณด์กด).
26
  SAMPLE_TEXT = (
27
  "โ–ถ์ •๋ถ€์˜ ๊ฐ•๋ ฅํ•œ ๋ถ€๋™์‚ฐ ๊ทœ์ œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ์ˆ˜๋„๊ถŒ ์•„ํŒŒํŠธ ๋งค๋งค ๊ฑฐ๋ž˜ ๋น„๋ฅ ์€ "
28
  "์˜คํžˆ๋ ค ์ฆ๊ฐ€์„ธ๋ฅผ ๋ณด์˜€๋‹ค. ์‹œ์ค‘ ์€ํ–‰์˜ ์ฃผํƒ ๋‹ด๋ณด ๋Œ€์ถœ ๊ธˆ๋ฆฌ๊ฐ€ ๋‚ฎ์•„์ง€๋ฉด์„œ "
29
  "์‹ค์ˆ˜์š”์ž์˜ ๊ตฌ๋งค ์‹ฌ๋ฆฌ๊ฐ€ ๋˜์‚ด์•„๋‚ฌ๊ธฐ ๋•Œ๋ฌธ์ด๋ผ๋Š” ๋ถ„์„์ด ๋‚˜์˜จ๋‹ค. "
30
  "๊ตญํ† ๋ถ€ ๊ด€๊ณ„์ž๋Š” โ€œ๋Œ€์ถœ ๊ฐฑ์‹ ๋ฅ ๊ณผ ์—ฐ์ฒด๋ฅ ์„ ํ•จ๊ป˜ ๋ด์•ผ ํ•œ๋‹คโ€๋ฉฐ โ€œ๋‹น๋ถ„๊ฐ„ ์ด๋Ÿฐ "
31
+ "ํ๋ฆ„์ด ์ด์–ด์งˆ ๊ฒƒโ€์ด๋ผ๊ณ  ๋งํ–ˆ๋‹ค. ์—…๊ณ„์—์„  'K-์ฃผ๊ฑฐ' ๋ชจ๋ธ์ด ํ•ด์™ธ์—์„œ๋„ ํ†ตํ• ์ง€ "
32
+ "์ฃผ๋ชฉํ•˜๊ณ  ์žˆ๋‹ค. ์‹ ๊ทœ ๊ด‘์—ญ์ฒ ๋„๋Š” ์ตœ๊ณ  ์‹œ์† 160ใŽž์œผ๋กœ ์„ค๊ณ„๋๋‹ค."
33
  )
34
 
35
+ # ์Šคํ… ์ด๋ฆ„ โ†’ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ๋ผ๋ฒจ (๋ชจ๋ฅด๋Š” ์Šคํ…์€ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ)
 
36
  STEP_LABELS = {
37
  "vocabulary_sub_pre": "์šฉ์–ด ์‚ฌ์ „ ์น˜ํ™˜",
38
+ "proofread": "1์ฐจ ๊ต์—ด (๊ต์—ด ์ „์šฉ FT ๋ชจ๋ธ, ๋ฌธ๋‹จ๋ณ„)",
39
+ "ft_hunks": "FT ์ˆ˜์ • ๋ชฉ๋ก ์ •๋ฆฌ",
40
+ "review": "๊ธฐ์‚ฌ ์ „์ฒด ๊ฒ€ํ†  (Claude Opus 1ํšŒ)",
41
+ "revert_ft": "FT ์˜ค๊ต์ • ๋˜๋Œ๋ฆผ",
42
+ "apply_review": "์‚ฌ์‹คยท์ˆซ์žยท๋ฌธ๋งฅ ๊ต์ • ์ ์šฉ",
43
+ "quote_direction": "๋”ฐ์˜ดํ‘œ ๋ฐฉํ–ฅ ์ •๋ฆฌ",
44
+ "quote_preserve": "์›๋ฌธ ๋”ฐ์˜ดํ‘œ ๋ณด์กด",
 
45
  "yul_ryul_normalize": "์œจ/๋ฅ  ๊ทœ์น™ ์ ์šฉ",
46
  "post_process": "ํ›„์ฒ˜๋ฆฌ (๋‹จ์œ„ยทํ†ตํ™”ยท์‰ผํ‘œ)",
 
 
47
  }
48
 
49
 
 
69
 
70
 
71
  class ProgressView:
72
+ """์Šคํ… ์ง„ํ–‰์„ st.status ์•ˆ์— ๊ทธ๋ฆฐ๋‹ค - ๋‹จ๊ณ„๋ณ„ ์ถœ๋ ฅ์€ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ณ  '์–ด๋””๊นŒ์ง€ ์™”๋‚˜' ๋งŒ."""
 
 
 
 
73
 
74
  def __init__(self, names: list[str]) -> None:
75
  self.names = names
76
+ self.done = 0
 
 
 
77
  self.status = st.status("๊ต์—ด ์ค‘์ž…๋‹ˆ๋‹คโ€ฆ", expanded=True)
78
+ self.bar = self.status.progress(0.0, text=f"0 / {len(names)} ๋‹จ๊ณ„")
79
  self.lines = self.status.empty()
80
  self._render()
81
 
82
+ def on_step(self, event: str, index: int, total: int, name: str) -> None:
83
+ if event == "done":
84
+ self.done = index
85
+ self.bar.progress(min(self.done / total, 1.0), text=f"{self.done} / {total} ๋‹จ๊ณ„")
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  self._render()
87
 
88
  def _render(self) -> None:
89
+ rows = []
90
+ for i, name in enumerate(self.names, 1):
91
+ label = STEP_LABELS.get(name, name)
92
+ if i <= self.done:
93
+ rows.append(f"โœ… {label}")
94
+ elif i == self.done + 1:
95
+ rows.append(f"โณ **{label}**")
96
+ else:
97
+ rows.append(f"โ–ซ๏ธ {label}")
 
 
 
 
 
 
 
 
 
 
 
 
98
  self.lines.markdown(" \n".join(rows))
99
 
100
  def finish(self, elapsed_s: float) -> None:
 
105
 
106
 
107
  @st.cache_resource(show_spinner="ํŒŒ์ดํ”„๋ผ์ธ ์ค€๋น„ ์ค‘โ€ฆ")
108
+ def get_pipeline():
109
+ return runner.build_pipeline()
 
110
 
111
 
112
  st.set_page_config(page_title="์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ", page_icon="๐Ÿ“ฐ", layout="wide")
113
  st.title("๐Ÿ“ฐ ์กฐ์„ ์ผ๋ณด ๋ณธ๋ฌธ ๊ต์—ด ๋ฐ๋ชจ")
114
+ st.caption(f"{runner.CONFIG.label} ยท ํ‰๊ฐ€ ํ•˜๋„ค์Šค์™€ ๊ฐ™์€ ์—”์ง„ ์ฝ”๋“œ")
115
 
116
  with st.sidebar:
117
  st.subheader("์„ค์ •")
118
+ for env_key in runner.REQUIRED_KEYS:
119
+ if not os.environ.get(env_key):
120
+ value = st.text_input(
121
+ env_key, type="password", help="HF Space ์—์„œ๋Š” Settings โ†’ Secrets ๋กœ ์„ค์ •ํ•˜์„ธ์š”."
122
+ )
123
+ if value:
124
+ os.environ[env_key] = value
125
+ cfg = runner.CONFIG
 
 
126
  st.markdown(f"**{cfg.label}**")
127
  st.caption(cfg.note)
128
+ st.markdown(f"- ํŒŒ์ดํ”„๋ผ์ธ `{cfg.pipeline}`\n- ํ”„๋กฌํ”„ํŠธ `{cfg.prompt}`")
129
 
130
+ text = st.text_area("๊ธฐ์‚ฌ ๋ณธ๋ฌธ", value=SAMPLE_TEXT, height=320, max_chars=12000)
131
 
132
  if st.button("๊ต์—ด ์‹คํ–‰", type="primary", disabled=not text.strip()):
133
+ missing = [k for k in runner.REQUIRED_KEYS if not os.environ.get(k)]
134
+ if missing:
135
  st.error(
136
+ f"{', '.join(missing)} ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค - ์‚ฌ์ด๋“œ๋ฐ”์— ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ Space Secrets ์— ์„ค์ •ํ•˜์„ธ์š”."
137
  )
138
  st.stop()
139
 
140
  source = text.strip()
141
+ pipeline = get_pipeline()
142
+ # ์ž…๋ ฅ์€ ์‹คํ–‰ ์ „์— ๋‚จ๊ธด๋‹ค - ์‹คํ–‰์ด ์‹คํŒจํ•ด๋„ ๋ฌด์—‡์ด ๋“ค์–ด์™”๋Š”์ง€๋Š” ์•Œ ์ˆ˜ ์žˆ๊ฒŒ
143
  article_id = logbook.save_article(source)
144
 
145
  progress = ProgressView(runner.step_names(pipeline))
146
  try:
147
+ result = runner.run_proofread(pipeline, source, on_step=progress.on_step)
148
+ except Exception as e: # noqa: BLE001 - ๋ฐ๋ชจ ํ‘œ๋ฉด์—์„œ๋Š” ์›์ธ ์š”์•ฝ๋งŒ ๋ณด์—ฌ์ค€๋‹ค
149
  progress.fail()
150
  st.error(f"์‹คํ–‰ ์‹คํŒจ: {type(e).__name__}: {e}")
151
  st.stop()
 
163
  st.caption(f"๊ธฐ๋ก ์‹คํŒจ (๊ฒฐ๊ณผ์—๋Š” ์˜ํ–ฅ ์—†์Œ): {logbook.last_error()}")
164
 
165
  left, right, _n_changed = diff_panes(source, result["output"])
 
166
  c1, c2 = st.columns(2, gap="medium")
167
  with c1:
168
  st.subheader("์›๋ฌธ")
assets/pipelines/pipeline_dev_v40.yaml DELETED
@@ -1,71 +0,0 @@
1
- name: pipeline_dev_v40
2
- description: "v38(C1) + ๊ทœ์น™ ์ธต 3์ข… (2026-09-18 ๋ผ์šด๋“œ): โ‘  vocabulary_v2.csv โ€” ํ‘œ์ค€์–ด/์‚ฌ์ด์‹œ์˜ท/์™ธ๋ž˜์–ด ํ˜ผ๋™ 71์Œ ๋“ฑ์žฌ (C3 ์‹คํŒจ๋กœ ์†Œ๊ฑฐ๋œ 'LLM ๊ธฐ์ดˆ๊ต์—ด ์ถ”๊ฐ€'์˜ ๊ทœ์น™ ๋Œ€์ฒด), โ‘ก josa_fix โ€” ๊ธฐํ˜ธยท์ˆซ์žยท๋‹จ์œ„ ๋’ค ์กฐ์‚ฌ ๊ฒฐ์ •๋ก  ๊ต์ • (ํ’€์…‹ gold 15๊ฑด ์ค‘ ๋ชจ๋ธ ์ ์ค‘ 1๊ฑด์ด๋˜ ํด๋ž˜์Šค), โ‘ข diff_revert โ€” ๊ธ€๋ฆฌํ”„ ๋ณ€ํ™˜ยท์‰ผํ‘œ ์‚ฝ์ž…ยท๊ธˆ์ง€ ์น˜ํ™˜ยท๊ฒฉ์‹ ์ „ํ™˜ ์˜คํƒ ๋˜๋Œ๋ฆผ (paragraph ์˜คํƒ 410๊ฑด ์ค‘ ~100๊ฑด์ด ์ด ํด๋ž˜์Šค). ํ’€์…‹ ์žฌ์ƒ(replay) ์‹ค์ธก: paragraph +4.71 / ci-v0 +3.67 / ci-v1 +1.78, rep-small 3๋ฐ˜๋ณต ์žฌ์ƒ +2.47. ์Šคํ… ์ถ”๊ฐ€๋ผ extends ๋ถˆ๊ฐ€, ์ „์ฒด ํŒŒ์ผ."
3
- text_split_size: 500
4
- steps:
5
- - name: vocabulary_sub_pre
6
- type: rule
7
- rule_type: vocabulary_sub
8
- config:
9
- vocab_dataset: resources/vocabulary_v2.csv
10
-
11
- - name: proofread
12
- type: llm
13
- model: ft:solar-news-correction
14
- prompt_key: proofread
15
- max_tokens: 2000
16
-
17
- - name: basic_correction
18
- type: llm
19
- prompt_key: step1
20
- user_template: |
21
- <์›๋ฌธ>
22
- {steps.vocabulary_sub_pre}
23
- </์›๋ฌธ>
24
-
25
- <๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>
26
- {current}
27
- </๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>
28
-
29
- - name: context_vocab
30
- type: llm
31
- prompt_key: step2a
32
-
33
- - name: context_grammar
34
- type: llm
35
- prompt_key: step2b
36
-
37
- - name: vocabulary_reapply
38
- type: rule
39
- config:
40
- vocab_dataset: resources/vocabulary_v2.csv
41
-
42
- - name: compound_whitelist_restore
43
- type: rule
44
- config:
45
- whitelist_path: resources/compound_whitelist.txt
46
-
47
- - name: yul_ryul_normalize
48
- type: rule
49
- config: {}
50
-
51
- - name: post_process
52
- type: rule
53
- config:
54
- pronoun_replacements:
55
- ์ด์žฌ๋ช… ๋Œ€ํ‘œ: ์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น
56
- currency_compact: true
57
- comma_removal: true
58
- unit_unicode: true
59
-
60
- - name: josa_fix
61
- type: rule
62
- config: {}
63
-
64
- - name: diff_revert
65
- type: rule
66
- config:
67
- glyph_restore: true
68
- comma_insertion_revert: true
69
- forbidden_insertions:
70
- - ์ฒญ์™€๋Œ€
71
- register_guard: true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/pipelines/pipeline_dev_v44.yaml DELETED
@@ -1,6 +0,0 @@
1
- name: pipeline_dev_v44
2
- description: "v41 + ๋ฌธ์„œ ๋‹จ์œ„ chunk โ€” ๋ฐ๋ชจ(HF Space) ํ‘œ๋ฉด์šฉ. ๋‚ด์šฉ์ƒ `pipeline_dev_v41` ๊ณผ ๋™์ผํ•˜๊ณ  `document_chunking: true` ํ•˜๋‚˜๋งŒ ๋‹ค๋ฅด๋‹ค. **base ๊ฐ€ v40 ์ธ ์ด์œ **: ์ƒ์†์€ 1๋‹จ๊ณ„๋กœ ์ œํ•œ๋ผ ์žˆ์–ด(core/pipeline_compose.py) v41(= v40 + punct ํ”Œ๋ž˜๊ทธ)์„ ๋‹ค์‹œ extends ํ•  ์ˆ˜ ์—†๋‹ค โ€” ๊ทธ๋ž˜์„œ v41 ์ด ์–น๋Š” ํ”Œ๋ž˜๊ทธ๋ฅผ ์—ฌ๊ธฐ์„œ ํ•จ๊ป˜ ์„ ์–ธํ•œ๋‹ค. ๋‘ ํŒŒ์ผ์€ diff_revert ์„ค์ •์ด ๊ฐˆ๋ผ์ง€๋ฉด ์•ˆ ๋˜๋ฏ€๋กœ v41 ์„ ๊ณ ์น  ๋•Œ ์ด ํŒŒ์ผ๋„ ๊ฐ™์ด ๋ณธ๋‹ค. chunk ๊ทผ๊ฑฐ: v35(= v34 + chunk)๊ฐ€ long-article ๋งค์นญ ฮ”F1 โˆ’0.9 โ†’ +3.5 (2026-09-10, `#260910_long_article_regression_set`). ๋‹จ v41 ๊ณ„์—ด์—์„œ์˜ ๊ธด ๊ธฐ์‚ฌ ๊ฒ€์ฆ์€ ์•„์ง ์—†๋‹ค(long-article ์…‹ ๋ฏธํ™•๋ณด)."
3
- extends: pipeline_dev_v40
4
- overrides:
5
- steps.diff_revert.config.punct_deletion_revert: true
6
- document_chunking: true
 
 
 
 
 
 
 
assets/pipelines/pipeline_exp_261021_ft_hybrid.yaml ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: pipeline_exp_261021_ft_hybrid
2
+ description: 'FT ๋ฌธ๋‹จ ์ธต + ๊ธฐ์‚ฌ ์ธต 1ํšŒ (2026-10-21) - ํ‘œ๊ธฐ ์‚ฌ์ „ ์น˜ํ™˜ ๋’ค ft:solar-news-correction ์„ ํ•™์Šต ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ(ft_system)๋กœ ์ค„๋ฐ”๊ฟˆ 500์ž ๋ฌถ์Œ๋งˆ๋‹ค ๋™์‹œ์— ๋ถ€๋ฅด๊ณ (๋ฌถ์Œ ๋™์‹œ 8), ์›๋ฌธ ๋Œ€๋น„ ๋ณ€๊ฒฝ ๋ชฉ๋ก(์‚ฌ์ „ ์น˜ํ™˜ ํฌํ•จ, ๋„์–ด์“ฐ๊ธฐ๋งŒ ๋‹ค๋ฅธ ๊ฒƒ์€ ๋บŒ)๊ณผ FT ๊ต์ •๋ณธ ์ „๋ฌธ์„ ๊ธฐ์‚ฌ ์ธต 1ํšŒ(review: FTยท์‚ฌ์ „ ์‚ฌ๊ณ  ๋˜๋Œ๋ฆผ ๋ฒˆํ˜ธ + FT ๊ฐ€ ๋ชป ๋ณด๋Š” ์‚ฌ์‹คยท์ˆซ์žยท๋ฌธ๋งฅ ๋‚ฑ๋ง ๊ต์ • ๋ชฉ๋ก)์— ์ค€๋‹ค. ๊ฒฐ์ •๋ก  ๋˜๋Œ๋ฆผยท์ ์šฉ ๋’ค ๋”ฐ์˜ดํ‘œ ๋ฐฉํ–ฅยท์œจ/๋ฅ ยทํ›„์ฒ˜๋ฆฌ ๊ทœ์น™. ๊ธฐ๋ณธ ๊ธฐ์‚ฌ ์ธต์€ Opus, Solar ๋งŒ์ด๋ฉด --set steps.review.model=solar-pro4 --set steps.review.prompt_key=review_sp4. ์ธก์ •ยท๋ณ€์ด ๊ธฐ๋ก์€ insights/261021_ft_hybrid_paragraph_article.md.'
3
+ text_split_size: 500
4
+ text_split_mode: lines
5
+ steps:
6
+ - name: vocabulary_sub_pre
7
+ type: rule
8
+ rule_type: vocabulary_sub
9
+ config:
10
+ vocab_dataset: resources/vocabulary.csv
11
+ - name: proofread
12
+ type: llm
13
+ model: ft:solar-news-correction
14
+ prompt_key: ft_system
15
+ max_tokens: 2000
16
+ temperature: 0.0
17
+ bulk_concurrency: 8
18
+ - name: ft_hunks
19
+ type: diff_hunks
20
+ config:
21
+ key: ft_diff
22
+ context: 20
23
+ max_hunks: 80
24
+ skip_spacing: true
25
+ - name: review
26
+ type: llm
27
+ model: timely:claude-opus-5-5
28
+ prompt_key: review
29
+ output_mode: json
30
+ max_tokens: 4000
31
+ user_template: |
32
+ <FT_๋ณ€๊ฒฝ_๋ชฉ๋ก>
33
+ {steps.ft_diff}
34
+ </FT_๋ณ€๊ฒฝ_๋ชฉ๋ก>
35
+
36
+ <๋ณธ๋ฌธ>
37
+ {current}
38
+ </๋ณธ๋ฌธ>
39
+ - name: revert_ft
40
+ type: revert_hunks
41
+ config:
42
+ hunks_from: ft_hunks
43
+ from_step: review
44
+ - name: apply_review
45
+ type: apply_corrections
46
+ config:
47
+ from_step: review
48
+ max_items: 12
49
+ max_shrink: 0.6
50
+ - name: quote_direction
51
+ type: rule
52
+ config: {}
53
+ - name: quote_preserve
54
+ type: rule
55
+ config: {}
56
+ - name: yul_ryul_normalize
57
+ type: rule
58
+ config: {}
59
+ - name: post_process
60
+ type: rule
61
+ config:
62
+ pronoun_replacements:
63
+ ์ด์žฌ๋ช… ๋Œ€ํ‘œ: ์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น
64
+ currency_compact: true
65
+ comma_removal: true
66
+ unit_unicode: true
assets/prompts/prompt_dev_260917_vsplit/step1.txt DELETED
@@ -1,85 +0,0 @@
1
- ๐Ÿšจ๐Ÿšจ๐Ÿšจ **์ตœ์ƒ์œ„ ํ•ต์‹ฌ ์ง€์‹œ์‚ฌํ•ญ (PRIME DIRECTIVE)** ๐Ÿšจ๐Ÿšจ๐Ÿšจ
2
- **์‚ฌ์‹ค ๋ณด์กด์ด ์ตœ์šฐ์„ ์ด๋‹ค.** `<์›๋ฌธ>` ์ด ๊ฐ€๋ฆฌํ‚ค๋Š” ์ธ๋ฌผยท๊ธฐ๊ด€ยท์ง์ฑ…ยท์ˆซ์žยท๋‚ ์งœยท์‚ฌ๊ฑด์€ ๋„ˆ์˜ ์ง€์‹๊ณผ ๋‹ค๋ฅด๋”๋ผ๋„ ์ ˆ๋Œ€ ๋‹ค๋ฅธ ๋Œ€์ƒ์œผ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. ์ด๋ฅผ ์œ„๋ฐ˜ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ค‘๋Œ€ํ•œ ์˜ค๋ฅ˜๋‹ค.
3
-
4
- ## Task
5
- ๋„ˆ๋Š” ํ•œ๊ตญ์–ด ๋‰ด์Šค ๊ต์—ด์˜ **๊ฒ€์ฆ์ž(verifier)** ๋‹ค. ๋‘ ํ…์ŠคํŠธ๋ฅผ ๋ฐ›๋Š”๋‹ค:
6
- - `<์›๋ฌธ>`: ๊ต์—ด๋˜์ง€ ์•Š์€ ์›๋ณธ. **๋ชจ๋“  ์‚ฌ์‹ค ์ •๋ณด(์ด๋ฆ„, ์ง์ฑ…, ์ˆซ์ž ๋“ฑ)์˜ ๊ธฐ์ค€.**
7
- - `<๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>`: 1์ฐจ ๊ต์—ด ๋ชจ๋ธ์ด ์ˆ˜์ •ํ•œ ๊ฒฐ๊ณผ. ์˜ฌ๋ฐ”๋ฅธ ๊ต์ •๊ณผ ์ž˜๋ชป๋œ ์ˆ˜์ •์ด ์„ž์—ฌ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.
8
-
9
- ๋„ˆ์˜ ์ž„๋ฌด๋Š” **์ƒˆ๋กœ ๊ต์ •ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, 1์ฐจ ๊ต์—ด์˜ ์ˆ˜์ •์„ ํ•˜๋‚˜์”ฉ ํŒ์ •ํ•˜๋Š” ๊ฒƒ**์ด๋‹ค.
10
- ๋‹ค์Œ 3๋‹จ๊ณ„๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ˆ˜ํ–‰ํ•˜๋ผ:
11
-
12
- 1. **์ฐจ์ด ๋‚˜์—ด**: `<์›๋ฌธ>` ๊ณผ `<๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>` ๋ฅผ ์–ด์ ˆ ๋‹จ์œ„๋กœ ๋Œ€์กฐํ•ด, 1์ฐจ ๊ต์—ด์ด ๋ฐ”๊พผ ์ง€์ ์„ **๋น ์ง์—†์ด** ๋‚˜์—ดํ•œ๋‹ค. ํ˜•์‹: `'์›๋ฌธ ํ‘œํ˜„' -> '์ˆ˜์ • ํ‘œํ˜„'`
13
- 2. **ํŒ์ •**: ๊ฐ ์ฐจ์ด์— ๋Œ€ํ•ด `keep`(์˜ฌ๋ฐ”๋ฅธ ๊ต์ •, ์œ ์ง€) ๋˜๋Š” `revert`(์ž˜๋ชป๋œ ์ˆ˜์ •, ์›๋ฌธ ๋ณต์›) ๋ฅผ ํŒ์ •ํ•˜๊ณ  ๊ทผ๊ฑฐ๋ฅผ ํ•œ ์ค„๋กœ ์ ๋Š”๋‹ค.
14
- 3. **์ตœ์ข… ์กฐ๋ฆฝ**: `keep` ํŒ์ •๋งŒ ๋ฐ˜์˜ํ•˜๊ณ  `revert` ํŒ์ •์€ ์›๋ฌธ ํ‘œํ˜„์œผ๋กœ ๋˜๋Œ๋ฆฐ ์ตœ์ข… ํ…์ŠคํŠธ๋ฅผ ๋งŒ๋“ ๋‹ค.
15
-
16
- **์ƒˆ ๊ต์ •์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์€ ๋„ˆ์˜ ์ž„๋ฌด๊ฐ€ ์•„๋‹ˆ๋‹ค.** 1์ฐจ ๊ต์—ด์ด ๋†“์นœ ์˜ค๋ฅ˜๋Š” ์ดํ›„ ๋‹จ๊ณ„๊ฐ€ ์ฒ˜๋ฆฌํ•œ๋‹ค. ์ฐจ์ด๊ฐ€ ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์ž…๋ ฅ์„ ๊ทธ๋Œ€๋กœ ์ถœ๋ ฅํ•œ๋‹ค.
17
-
18
- ## ํŒ์ • ๊ธฐ์ค€
19
-
20
- **keep (์˜ฌ๋ฐ”๋ฅธ ๊ต์ • โ€” ์œ ์ง€):**
21
- - ๋งž์ถค๋ฒ•ยท์˜คํƒˆ์ž ๊ต์ • (์˜ˆ: `๊ถŒ์˜์ฃผ์˜` -> `๊ถŒ์œ„์ฃผ์˜`, `๊ธฐ๋ ท๋‹ค` -> `๊ธฐ๋ ธ๋‹ค`)
22
- - ์‚ฌ์ด์‹œ์˜ท ๊ต์ • (์˜ˆ: `์ฝ”์ˆ˜์—ผ` -> `์ฝง์ˆ˜์—ผ`, `๋Œ“๊ฐ€` -> `๋Œ€๊ฐ€` โ€” ๋„ฃ์„ ๊ณณ๊ณผ ๋บ„ ๊ณณ ๋ชจ๋‘)
23
- - ์กฐ์‚ฌยท์–ด๋ฏธ ๊ต์ • (์˜ˆ: `1๋ฅผ` -> `1์„`, `~ํ–ˆ๋‹ค๊ณ  ํ–ˆ๋‹ค` ์˜ ํ˜ธ์‘ ๊ต์ •)
24
- - ๋ช…๋ฐฑํ•œ ๋„์–ด์“ฐ๊ธฐ ์˜ค๋ฅ˜ ๊ต์ •
25
- - ๊ฐ™์€ ๋Œ€์ƒ์˜ ํ‘œ๊ธฐ ์˜ค๋ฅ˜ ๊ต์ • (์˜ˆ: `์˜ค๋‹ˆ์นด์™€` -> `์˜คํ‚ค๋‚˜์™€`, `์‰ฅ๊ฒ` -> `์†…๊ฒ` โ€” ๋Œ€์ƒ์ด ๊ฐ™๊ณ  ํ‘œ๊ธฐ๋งŒ ํ‹€๋ ธ์„ ๋•Œ)
26
-
27
- **revert (์ž˜๋ชป๋œ ์ˆ˜์ • โ€” ์›๋ฌธ ๋ณต์›):**
28
- - **๋Œ€์ƒ ๊ต์ฒด**: ์ธ๋ฌผยท๊ธฐ๊ด€ยท์ง์ฑ…ยท์‚ฌ๊ฑด์„ ๋‹ค๋ฅธ ๋Œ€์ƒ์œผ๋กœ ๋ฐ”๊พผ ๊ฒƒ (์˜ˆ: `์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น` -> `์œค์„์—ด ๋Œ€ํ†ต๋ น`, `๊ธฐ์‹œ๋‹ค ์ด๋ฆฌ` -> `๋‹ค์นด์ด์น˜ ์ด๋ฆฌ`). ๋„ˆ์˜ ์ง€์‹์ƒ ์›๋ฌธ์ด ํ‹€๋ ค ๋ณด์—ฌ๋„ **๋ฌด์กฐ๊ฑด revert**.
29
- - **์ˆซ์žยท๋‚ ์งœยท๋‹จ์œ„ ๊ฐ’ ๋ณ€๊ฒฝ**: ์ˆ˜์น˜ ์ž์ฒด๋ฅผ ๋ฐ”๊พผ ๊ฒƒ (ํ‘œ๊ธฐ ์ •๊ทœํ™”๊ฐ€ ์•„๋‹ˆ๋ผ ๊ฐ’์ด ๋‹ฌ๋ผ์ง„ ๊ฒฝ์šฐ)
30
- - **๋™์˜์–ด ์น˜ํ™˜ยท์œค๋ฌธ**: ์›๋ฌธ ํ‘œํ˜„๋„ ํ‘œ์ค€์–ด์ด๊ณ  ๋œป์ด ํ†ตํ•˜๋Š”๋ฐ ๋‹ค๋ฅธ ํ‘œํ˜„์œผ๋กœ ๋ฐ”๊พผ ๊ฒƒ (์˜ˆ: `์žฌ์ฐจ` -> `๋‹ค์‹œ`)
31
- - **๋ฌธ์žฅ ์••์ถ•ยท๋‚ด์šฉ ์‚ญ์ œยท๋‚ด์šฉ ์ถ”๊ฐ€**: ์›๋ฌธ์— ์žˆ๋˜ ๋‚ด์šฉ์ด ์‚ฌ๋ผ์ง€๊ฑฐ๋‚˜ ์—†๋˜ ๋‚ด์šฉ(๊ด„ํ˜ธ ๋ถ€์—ฐ ํฌํ•จ)์ด ์ƒ๊ธด ๊ฒƒ
32
- - **ํ—ˆ์šฉ๋˜๋Š” ํ‘œ๊ธฐ๋ฅผ ๋ฐ”๊พผ ๊ฒƒ**: ๋ถ™์—ฌ ์”€/๋„์–ด ์”€์ด ๋ชจ๋‘ ํ—ˆ์šฉ๋˜๋Š” ๋ณตํ•ฉ์–ด์˜ ์žฌ์กฐ์ •
33
- - **๋ฌธ๋‹จยท๋ฌธ์žฅ ๊ตฌ์กฐ ๋ณ€๊ฒฝ**: `<paragraph_separator>` ๋‚˜ ์ค„๋ฐ”๊ฟˆ์˜ ์ถ”๊ฐ€ยท์‚ญ์ œยท์ด๋™
34
-
35
- ํŒ๋‹จ์ด ํ™•์‹คํ•˜์ง€ ์•Š์œผ๋ฉด **revert** ํ•œ๋‹ค โ€” ์›๋ฌธ ์œ ์ง€๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์ด๋‹ค.
36
-
37
- ## ์ถœ๋ ฅ ํ˜•์‹
38
-
39
- ๋จผ์ € ํŒ์ • ๋ชฉ๋ก์„ ์ž์œ  ํ…์ŠคํŠธ๋กœ ์ ์€ ๋’ค, **๋งˆ์ง€๋ง‰์— JSON ํ•˜๋‚˜**๋ฅผ ์ถœ๋ ฅํ•œ๋‹ค.
40
- - `output`: ์ตœ์ข… ํ…์ŠคํŠธ ์ „์ฒด. `<paragraph_separator>` ์™€ ์ค„๋ฐ”๊ฟˆ ๊ตฌ์กฐ๋Š” ์›๋ฌธ๊ณผ 100% ๋™์ผํ•˜๊ฒŒ ์œ ์ง€ (`[IMG:~~]` ํƒœ๊ทธ ํฌํ•จ).
41
- - `explanation`: revert ํ•œ ํ•ญ๋ชฉ ์š”์•ฝ. ์—†์œผ๋ฉด `"๋ชจ๋“  ์ˆ˜์ • ์œ ์ง€"`. ์ค„๋ฐ”๊ฟˆ์€ `\n` ์œผ๋กœ ํ‘œ๊ธฐ.
42
-
43
- ## ์˜ˆ์‹œ
44
-
45
- **Input:**
46
- ```xml
47
- <์›๋ฌธ>
48
- ์œค์„์—ด ๋Œ€ํ†ต๋ น์€ ์ด๋‚  ์ ์˜ค์— ๋ชจ์—ฌ ํšŒ์˜๋ฅผ ๊ธฐ๋ ท๋‹ค.
49
- </์›๋ฌธ>
50
- <๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>
51
- ์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น์€ ์ด๋‚  ์ •์˜ค์— ๋ชจ์—ฌ ํšŒ์˜๋ฅผ ๊ธฐ๋ ธ๋‹ค.
52
- </๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>
53
- ```
54
-
55
- **CORRECT EXAMPLE:**
56
- ์ฐจ์ด ํŒ์ •:
57
- 1. '์œค์„์—ด ๋Œ€ํ†ต๋ น์€' -> '์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น์€' : revert (๋Œ€์ƒ ๊ต์ฒด โ€” ์›๋ฌธ์˜ ์ธ๋ฌผ์„ ๋‹ค๋ฅธ ์ธ๋ฌผ๋กœ ๋ณ€๊ฒฝ)
58
- 2. '์ ์˜ค์—' -> '์ •์˜ค์—' : keep (์˜คํƒˆ์ž ๊ต์ •)
59
- 3. '๊ธฐ๋ ท๋‹ค' -> '๊ธฐ๋ ธ๋‹ค' : keep (๋งž์ถค๋ฒ• ๊ต์ •)
60
-
61
- {"output": "์œค์„์—ด ๋Œ€ํ†ต๋ น์€ ์ด๋‚  ์ •์˜ค์— ๋ชจ์—ฌ ํšŒ์˜๋ฅผ ๊ธฐ๋ ธ๋‹ค.", "explanation": "'์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น์€' -> '์œค์„์—ด ๋Œ€ํ†ต๋ น์€' (๋Œ€์ƒ ๊ต์ฒด ๋ณต์›)"}
62
-
63
- **Input:**
64
- ```xml
65
- <์›๋ฌธ>
66
- ๊ทธ๋Š” ์ Š์€ ์‹œ์ ˆ ๊ถŒ์œ„์ฃผ์˜์— ๋งž์„œ ์žฌ์ฐจ ํˆฌ์Ÿํ–ˆ๋‹ค.
67
- </์›๋ฌธ>
68
- <๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>
69
- ๊ทธ๋Š” ์ Š์€ ์‹œ์ ˆ ๊ถŒ์œ„์ฃผ์˜์— ๋งž์„œ ๋‹ค์‹œ ํˆฌ์Ÿํ–ˆ๋‹ค.
70
- </๊ต์—ด_๋ชจ๋ธ_์ˆ˜์ •๊ฒฐ๊ณผ>
71
- ```
72
-
73
- **CORRECT EXAMPLE:**
74
- ์ฐจ์ด ํŒ์ •:
75
- 1. '์žฌ์ฐจ' -> '๋‹ค์‹œ' : revert (๋™์˜์–ด ์น˜ํ™˜ โ€” ์›๋ฌธ ํ‘œํ˜„๋„ ํ‘œ์ค€์–ด)
76
-
77
- {"output": "๊ทธ๋Š” ์ Š์€ ์‹œ์ ˆ ๊ถŒ์œ„์ฃผ์˜์— ๋งž์„œ ์žฌ์ฐจ ํˆฌ์Ÿํ–ˆ๋‹ค.", "explanation": "'๋‹ค์‹œ' -> '์žฌ์ฐจ' (๋™์˜์–ด ์น˜ํ™˜ ๋ณต์›)"}
78
-
79
- ------------------------------------
80
-
81
- โš ๏ธ ์ตœ์ข… ํ™•์ธ:
82
- - ํŒ์ • ๋ชฉ๋ก ๋‹ค์Œ, ๋งˆ์ง€๋ง‰์— JSON ํ•œ ๊ฐœ๋งŒ ์ถœ๋ ฅํ•œ๋‹ค (JSON ๋’ค์— ๋‹ค๋ฅธ ํ…์ŠคํŠธ ๊ธˆ์ง€)
83
- - `output` ์˜ `<paragraph_separator>` ๊ฐœ์ˆ˜ยท์œ„์น˜ยท์ค„๋ฐ”๊ฟˆ์€ ์›๋ฌธ๊ณผ 100% ๋™์ผ
84
- - ์ฐจ์ด๊ฐ€ ์—†์œผ๋ฉด: `{"output": "[์ž…๋ ฅ ๊ทธ๋Œ€๋กœ]", "explanation": "์ˆ˜์ •๋œ ๋‚ด๏ฟฝ๏ฟฝ๏ฟฝ์ด ์—†์Šต๋‹ˆ๋‹ค."}`
85
- - ์ƒˆ ๊ต์ • ์ถ”๊ฐ€ ๊ธˆ์ง€ โ€” ๋‚˜์—ดยทํŒ์ •ยท์กฐ๋ฆฝ๋งŒ ํ•œ๋‹ค
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/prompts/prompt_dev_260917_vsplit/step2a.txt DELETED
@@ -1,36 +0,0 @@
1
- ๐Ÿšจ๐Ÿšจ๐Ÿšจ **์ตœ์ƒ์œ„ ํ•ต์‹ฌ ์ง€์‹œ์‚ฌํ•ญ (PRIME DIRECTIVE)** ๐Ÿšจ๐Ÿšจ๐Ÿšจ
2
- 1. **์‚ฌ์‹ค ์ •๋ณด ์ˆ˜์ • ์ ˆ๋Œ€ ๊ธˆ์ง€**: ์ธ๋ฌผ, ์ง์ฑ…, ์ˆซ์ž, ๊ณ ์œ ๋ช…์‚ฌ๋Š” ๋„ˆ์˜ ์ง€์‹๊ณผ ๋‹ค๋ฅด๋”๋ผ๋„ ์ ˆ๋Œ€ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. (์ž…๋ ฅ `์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น` โ†’ ์ถœ๋ ฅ `์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น`)
3
- 2. **๊ตฌ์กฐ ์ ˆ๋Œ€ ์œ ์ง€**: ๋ฌธ๋‹จ ์ˆ˜ยท๋ฌธ์žฅ ์ˆœ์„œยท์ค„๋ฐ”๊ฟˆยท`<paragraph_separator>`ยท`[IMG:~~]` ํƒœ๊ทธ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•œ๋‹ค. ๋‚ด์šฉ ์ถ”๊ฐ€(๊ด„ํ˜ธ ๋ถ€์—ฐ ํฌํ•จ)ยท์‚ญ์ œ ๊ธˆ์ง€.
4
- 3. **์ˆ˜์ •ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ๊ธฐ๋ณธ๊ฐ’์ด๋‹ค**: ๋™์˜์–ด ์น˜ํ™˜, ๋ฌธ์žฅ ์••์ถ•, ๋„์–ด์“ฐ๊ธฐ ์žฌ์กฐ์ •, ํ‘œํ˜„ ๋‹ค๋“ฌ๊ธฐ๋Š” ์ „๋ถ€ ๊ธˆ์ง€. ์•„๋ž˜ ์ž„๋ฌด์— ํ•ด๋‹นํ•˜๋Š” ๊ต์ •๋งŒ ํ•œ๋‹ค.
5
-
6
- # ์—ญํ• 
7
- ๋‹น์‹ ์€ ๋‰ด์Šค ๊ธฐ์‚ฌ ๊ต์—ด๊ฐ€ ์ค‘ **์–ดํœ˜ ์˜ค์‹(่ชคๆค) ์ „๋‹ด**์ž…๋‹ˆ๋‹ค. ๊ธฐ์ดˆ ๊ต์—ด์ด ๋๋‚œ ํ…์ŠคํŠธ์—์„œ **๋‹จ ํ•œ ๊ฐ€์ง€**๋งŒ ์ฐพ์Šต๋‹ˆ๋‹ค: **๋ฌธ๋งฅ์— ๋œป์ด ํ†ตํ•˜์ง€ ์•Š๋Š” ๋‹จ์–ด**.
8
-
9
- ์‹ ๋ฌธ ์ œ์ž‘ ๊ณผ์ •์—์„œ๋Š” ๋ฐœ์Œยทํ‘œ๊ธฐ๊ฐ€ ๋น„์Šทํ•œ ๋‹ค๋ฅธ ๋‹จ์–ด๋กœ ์ž˜๋ชป ์ฐํžˆ๋Š” ์˜ค์‹์ด ์žฆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์žฅ์„ ์ฝ์œผ๋ฉฐ ๋œป์ด ์–ด๊ธ‹๋‚˜๋Š” ๋‹จ์–ด๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด, ๋ฐœ์Œ์ด ๋น„์Šทํ•˜๊ณ (๋Œ€๊ฐœ ํ•œ๋‘ ๊ธ€์ž ์ฐจ์ด) ๋œป์ด ๋ช…๋ฐฑํžˆ ํ†ตํ•˜๋Š” ๋‹จ์–ด๋กœ ๊ต์ •ํ•˜์„ธ์š”. **ํ˜„์žฌ ๋‹จ์–ด๋กœ๋„ ๋œป์ด ํ†ตํ•˜๋ฉด ์ ˆ๋Œ€ ๋ฐ”๊พธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค** โ€” ์ด ์ž‘์—…์€ ์˜ค์‹ ๋ณต์›์ด์ง€ ์–ดํœ˜ ์„ ํ˜ธ ๊ต์ฒด๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.
10
-
11
- # ๊ต์ • ์˜ˆ์‹œ (์˜ค์‹ ๋ณต์›)
12
- - ๊ณคํ˜น์„ ์น˜๋ฅด๋‹ค โ†’ ๊ณค์š•์„ ์น˜๋ฅด๋‹ค (์น˜๋ฅด๋Š” ๊ฒƒ์€ '๊ณค์š•')
13
- - ๊ฒ‰์žก์„ ์ˆ˜ ์—†์ด โ†’ ๊ฑท์žก์„ ์ˆ˜ ์—†์ด
14
- - ์ •๋‹ต์„ ๋งž์ถ”๋‹ค โ†’ ์ •๋‹ต์„ ๋งžํžˆ๋‹ค (์ ์ค‘์€ '๋งžํžˆ๋‹ค')
15
- - ์ˆ˜์ถœ๋Ÿ‰์ด ๊ธ‰๋“ฑํ•˜๋‹ค โ†’ ์ˆ˜์ถœ๋Ÿ‰์ด ๊ธ‰์ฆํ•˜๋‹ค (์ˆ˜๋Ÿ‰์€ ๊ธ‰์ฆ/๊ธ‰๊ฐ, ๊ฐ€๊ฒฉยท์ง€์ˆ˜๋Š” ๊ธ‰๋“ฑ/๊ธ‰๋ฝ)
16
- - ์‹ค์ˆ˜๊ฐ€ ๋ฒˆ๋ณต๋˜๋‹ค โ†’ ์‹ค์ˆ˜๊ฐ€ ๋ฐ˜๋ณต๋˜๋‹ค (๋˜ํ’€์ด๋Š” '๋ฐ˜๋ณต', ๊ฒฐ์ • ๋’ค์ง‘๊ธฐ๋Š” '๋ฒˆ๋ณต')
17
- - ์‹ ๋ฌธ์— ๊ฐœ์žฌ๋œ ๊ธฐ์‚ฌ โ†’ ์‹ ๋ฌธ์— ๊ฒŒ์žฌ๋œ ๊ธฐ์‚ฌ
18
- - ์œ ๋ž˜์—†๋Š” ํญ์—ผ โ†’ ์œ ๋ก€์—†๋Š” ํญ์—ผ (๋น„์Šทํ•œ ์‚ฌ๋ก€๋Š” '์œ ๋ก€')
19
- - ์Šน์ „๋ณด๋ฅผ ์šธ๋ฆฌ๋‹ค โ†’ ์Šน์ „๊ณ ๋ฅผ ์šธ๋ฆฌ๋‹ค
20
- - ๊ฐ์ถœํ•˜๋‹ค โ†’ ๊ฐน์ถœํ•˜๋‹ค (์—ฌ๋Ÿฟ์ด ๋‚˜๋ˆ„์–ด ๋‚ผ ๋•Œ)
21
- - ๋งค์—ฌ โ†’ ๋ฉ”์–ด (์–ด๊นจ์— ๊ฑธ ๋•Œ๋Š” '๋ฉ”๋‹ค')
22
-
23
- # ๋ฐ”๊พธ๋ฉด ์•ˆ ๋˜๋Š” ์˜ˆ (๋œป์ด ํ†ตํ•จ โ€” ์œ ์ง€)
24
- - '์žฌ์ฐจ ๊ฐ•์กฐํ–ˆ๋‹ค' โ†’ ๊ทธ๋Œ€๋กœ ์œ ์ง€ ('์žฌ์ฐจ'๋„ ํ‘œ์ค€์–ด๋‹ค. '๋‹ค์‹œ'๋กœ ๋ฐ”๊พธ์ง€ ๋งˆ๋ผ)
25
- - '๊ตฌ๋ณ„ํ•˜๋‹ค' โ†’ ๋ฌธ๋งฅ์ƒ ๋œป์ด ํ†ตํ•˜๋ฉด ๊ทธ๋Œ€๋กœ ์œ ์ง€
26
- - ์œจ/๋ฅ  ํ‘œ๊ธฐ, ๋„์–ด์“ฐ๊ธฐ, ์‚ฌ์ด์‹œ์˜ท โ†’ ์ด ๋‹จ๊ณ„์˜ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค. ์†๋Œ€์ง€ ๋งˆ๋ผ
27
-
28
- # ์ถœ๋ ฅ ํ˜•์‹
29
- JSON ํ•˜๋‚˜๋งŒ ์ถœ๋ ฅํ•œ๋‹ค (๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋กยท์„ค๋ช…๋ฌธ ๊ธˆ์ง€):
30
- {"output": "[๊ต์ • ์™„๋ฃŒ๋œ ์ „์ฒด ํ…์ŠคํŠธ - ์ˆ˜์ • ์—†์–ด๋„ ์ž…๋ ฅ ์ „์ฒด ํฌํ•จ, <paragraph_separator>ยท์ค„๋ฐ”๊ฟˆ์€ ์ž…๋ ฅ๊ณผ 100% ๋™์ผ]", "explanation": "['๊ธฐ์กด' -> '์ˆ˜์ •' (์ด์œ )' ํ˜•์‹, ์—ฌ๋Ÿฌ ๊ฐœ๋Š” \\n ๊ตฌ๋ถ„, ์—†์œผ๋ฉด '์ˆ˜์ •๋œ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค.']"}
31
-
32
- **Input:** ์‚ฌ๊ณ  ์—ฌํŒŒ๊ฐ€ ๊ฒ‰์žก์„ ์ˆ˜ ์—†์ด ์ปค์ง€์ž ์‹œ๋Š” ๊ณคํ˜น์„ ์น˜๋ €๋‹ค.
33
- **CORRECT:** {"output": "์‚ฌ๊ณ  ์—ฌํŒŒ๊ฐ€ ๊ฑท์žก์„ ์ˆ˜ ์—†์ด ์ปค์ง€์ž ์‹œ๋Š” ๊ณค์š•์„ ์น˜๋ €๋‹ค.", "explanation": "'๊ฒ‰์žก์„' -> '๊ฑท์žก์„' (์˜ค์‹ ๋ณต์›)\n'๊ณคํ˜น์„' -> '๊ณค์š•์„' (์น˜๋ฅด๋Š” ๊ฒƒ์€ '๊ณค์š•')"}
34
-
35
- **Input:** KํŒ ๊ทธ๋ฃน์ด ์ปด๋ฐฑํ–ˆ๋‹ค.
36
- **CORRECT:** {"output": "KํŒ ๊ทธ๋ฃน์ด ์ปด๋ฐฑํ–ˆ๋‹ค.", "explanation": "์ˆ˜์ •๋œ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/prompts/prompt_dev_260917_vsplit/step2b.txt DELETED
@@ -1,33 +0,0 @@
1
- ๐Ÿšจ๐Ÿšจ๐Ÿšจ **์ตœ์ƒ์œ„ ํ•ต์‹ฌ ์ง€์‹œ์‚ฌํ•ญ (PRIME DIRECTIVE)** ๐Ÿšจ๐Ÿšจ๐Ÿšจ
2
- 1. **์‚ฌ์‹ค ์ •๋ณด ์ˆ˜์ • ์ ˆ๋Œ€ ๊ธˆ์ง€**: ์ธ๋ฌผ, ์ง์ฑ…, ์ˆซ์ž, ๊ณ ์œ ๋ช…์‚ฌ๋Š” ๋„ˆ์˜ ์ง€์‹๊ณผ ๋‹ค๋ฅด๋”๋ผ๋„ ์ ˆ๋Œ€ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. (์ž…๋ ฅ `์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น` โ†’ ์ถœ๋ ฅ `์ด์žฌ๋ช… ๋Œ€ํ†ต๋ น`)
3
- 2. **๊ตฌ์กฐ ์ ˆ๋Œ€ ์œ ์ง€**: ๋ฌธ๋‹จ ์ˆ˜ยท๋ฌธ์žฅ ์ˆœ์„œยท์ค„๋ฐ”๊ฟˆยท`<paragraph_separator>`ยท`[IMG:~~]` ํƒœ๊ทธ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•œ๋‹ค. ๋‚ด์šฉ ์ถ”๊ฐ€(๊ด„ํ˜ธ ๋ถ€์—ฐ ํฌํ•จ)ยท์‚ญ์ œ ๊ธˆ์ง€.
4
- 3. **์ˆ˜์ •ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ๊ธฐ๋ณธ๊ฐ’์ด๋‹ค**: ๋™์˜์–ด ์น˜ํ™˜, ๋ฌธ์žฅ ์••์ถ•, ์–ด์ˆœ ์žฌ๋ฐฐ์น˜, ํ‘œํ˜„ ๋‹ค๋“ฌ๊ธฐ, ๋„์–ด์“ฐ๊ธฐ ์žฌ์กฐ์ •์€ ์ „๋ถ€ ๊ธˆ์ง€. ์•„๋ž˜ ์ž„๋ฌด์— ํ•ด๋‹นํ•˜๋Š” ๊ต์ •๋งŒ ํ•œ๋‹ค.
5
-
6
- # ์—ญํ• 
7
- ๋‹น์‹ ์€ ๋‰ด์Šค ๊ธฐ์‚ฌ ๊ต์—ด๊ฐ€ ์ค‘ **๋ฌธ๋ฒ• ํ˜ธ์‘ ์ „๋‹ด**์ž…๋‹ˆ๋‹ค. ๊ธฐ์ดˆ ๊ต์—ด์ด ๋๋‚œ ํ…์ŠคํŠธ์—์„œ ๋‹ค์Œ ๋„ค ๊ฐ€์ง€๋งŒ ์ฐพ์Šต๋‹ˆ๋‹ค:
8
-
9
- 1. **์กฐ์‚ฌ ์˜ค๋ฅ˜ยท๋ˆ„๋ฝ**: ๋ฐ›์นจ์— ๋งž์ง€ ์•Š๋Š” ์กฐ์‚ฌ, ๋ฌธ๋งฅ์— ์•ˆ ๋งž๋Š” ์กฐ์‚ฌ, ๋น ์ง„ ์กฐ์‚ฌ
10
- - ์˜ˆ: `1๋ฅผ` โ†’ `1์„` / `ํ•˜๋ฅด๊ทธ์„ฌ์ด ์›์œ  ์ˆ˜์ถœ ์‹ฌ์žฅ๋ถ€` โ†’ `ํ•˜๋ฅด๊ทธ์„ฌ์˜ ์›์œ  ์ˆ˜์ถœ ์‹ฌ์žฅ๋ถ€` (์†Œ์œ  ๊ด€๊ณ„)
11
- 2. **์–ด๋ฏธยท์šฉ์–ธ ํ™œ์šฉ ์˜ค๋ฅ˜**: ์ž˜๋ชป๋œ ํ™œ์šฉํ˜•, ๊ฒน์นœ ์–ด๋ฏธ
12
- - ์˜ˆ: `๊ฐ€๊ฒ ๋‹ค"๋ฉฐ๋ฉฐ` โ†’ `๊ฐ€๊ฒ ๋‹ค"๋ฉฐ` / `์Šฌํ”„์ž` โ†’ `์Šฌํผ์„œ` / `๋ชจ์ž๋ผ์•ˆ ํ•˜๋‹ค` โ†’ `๋ชจ์ž๋ผ์ง€ ์•Š๋‹ค`
13
- 3. **๋ฌธ์žฅ ์„ฑ๋ถ„ ํ˜ธ์‘ ์˜ค๋ฅ˜**: ์ฃผ์–ด-์„œ์ˆ ์–ด, ๋ชฉ์ ์–ด-์„œ์ˆ ์–ด๊ฐ€ ์–ด๊ธ‹๋‚˜๋Š” ๊ฒฝ์šฐ โ€” **์–ด๊ธ‹๋‚œ ์„ฑ๋ถ„๋งŒ ์ตœ์†Œ ์ˆ˜์ •**
14
- - ์˜ˆ: `์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š”๊ฐ€ ์ค‘์š”ํ•˜๋‹ค` โ†’ `์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•˜๋Š”์ง€๊ฐ€ ์ค‘์š”ํ•˜๋‹ค`
15
- 4. **๋‹จ์œ„ ๋ˆ„๋ฝ**: ์ˆ˜์น˜ ๋’ค์— ๋‹จ์œ„๊ฐ€ ๋น ์ง„ ๊ฒฝ์šฐ โ€” ๋ฌธ๋งฅ์— ๋ช…์‹œ๋œ ๋‹จ์œ„๋งŒ ๋ณด์ถฉ
16
- - ์˜ˆ: `์‹œ์žฅ ๊ทœ๋ชจ๋Š” 1์กฐ7000์–ต์ด๋‹ค` โ†’ `์‹œ์žฅ ๊ทœ๋ชจ๋Š” 1์กฐ7000์–ต์›์ด๋‹ค` (์•ž ๋ฌธ๋งฅ์ด ๊ธˆ์•ก์ผ ๋•Œ)
17
-
18
- **์‚ฌ๋™/ํ”ผ๋™ ํ˜ผ์šฉ**์€ ๋ช…๋ฐฑํžˆ ๋ฌธ๋ฒ•์ด ํ‹€๋ฆฐ ๊ฒฝ์šฐ๋งŒ ๊ณ ์นฉ๋‹ˆ๋‹ค (์˜ˆ: `๋ณด์—ฌ์ง€๋‹ค` โ†’ `๋ณด์ด๋‹ค`). ๋‘˜ ๋‹ค ํ—ˆ์šฉ๋˜๋Š” ํ‘œํ˜„์€ ๊ทธ๋Œ€๋กœ ๋‘ก๋‹ˆ๋‹ค.
19
-
20
- # ๋ฐ”๊พธ๋ฉด ์•ˆ ๋˜๋Š” ์˜ˆ (์œ ์ง€)
21
- - ๋œป์ด ํ†ตํ•˜๋Š” ๋ฌธ์žฅ์˜ ์–ด์ˆœ โ†’ ๊ทธ๋Œ€๋กœ ์œ ์ง€ (์žฌ๋ฐฐ์น˜๋Š” ๋„ˆ์˜ ์ž„๋ฌด๊ฐ€ ์•„๋‹ˆ๋‹ค)
22
- - '~์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ ', ๋ฒˆ์—ญํˆฌ๋กœ ๋ณด์ด๋Š” ํ‘œํ˜„ โ†’ ๋ฌธ๋ฒ•์ด ํ‹€๋ฆฌ์ง€ ์•Š์•˜์œผ๋ฉด ์œ ์ง€
23
- - ๋„์–ด์“ฐ๊ธฐยท์‚ฌ์ด์‹œ์˜ทยท๋งž์ถค๋ฒ• โ†’ ์ด ๋‹จ๊ณ„์˜ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค. ์†๋Œ€์ง€ ๋งˆ๋ผ
24
-
25
- # ์ถœ๋ ฅ ํ˜•์‹
26
- JSON ํ•˜๋‚˜๋งŒ ์ถœ๋ ฅํ•œ๋‹ค (๋งˆํฌ๋‹ค์šด ์ฝ”๋“œ ๋ธ”๋กยท์„ค๋ช…๋ฌธ ๊ธˆ์ง€):
27
- {"output": "[๊ต์ • ์™„๋ฃŒ๋œ ์ „์ฒด ํ…์ŠคํŠธ - ์ˆ˜์ • ์—†์–ด๋„ ์ž…๋ ฅ ์ „์ฒด ํฌํ•จ, <paragraph_separator>ยท์ค„๋ฐ”๊ฟˆ์€ ์ž…๋ ฅ๊ณผ 100% ๋™์ผ]", "explanation": "['๊ธฐ์กด' -> '์ˆ˜์ •' (์ด์œ )' ํ˜•์‹, ์—ฌ๋Ÿฌ ๊ฐœ๋Š” \\n ๊ตฌ๋ถ„, ์—†์œผ๋ฉด '์ˆ˜์ •๋œ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค.']"}
28
-
29
- **Input:** ๋ฒ•์›์€ ํ˜์˜ 11๊ฐœ์— ๋Œ€ํ•ด ์œ ์ฃ„ ํŒ๊ฒฐ์„ ๋‚ด๋ ธ๊ณ  ์กฐ๊ตญ์€ "๊ฐ€์‹œ๋ฐญ๊ธธ์„ ๊ฐ€๊ฒ ๋‹ค"๋ฉฐ๋ฉฐ ํ•ญ์†Œํ–ˆ๋‹ค.
30
- **CORRECT:** {"output": "๋ฒ•์›์€ ํ˜์˜ 11๊ฐœ์— ๋Œ€ํ•ด ์œ ์ฃ„ ํŒ๊ฒฐ์„ ๋‚ด๋ ธ๊ณ  ์กฐ๊ตญ์€ \"๊ฐ€์‹œ๋ฐญ๊ธธ์„ ๊ฐ€๊ฒ ๋‹ค\"๋ฉฐ ํ•ญ์†Œํ–ˆ๋‹ค.", "explanation": "'๊ฐ€๊ฒ ๋‹ค\"๋ฉฐ๋ฉฐ' -> '๊ฐ€๊ฒ ๋‹ค\"๋ฉฐ' (๊ฒน์นœ ์–ด๋ฏธ ๊ต์ •)"}
31
-
32
- **Input:** KํŒ ๊ทธ๋ฃน์ด ์ปด๋ฐฑํ–ˆ๋‹ค.
33
- **CORRECT:** {"output": "KํŒ ๊ทธ๋ฃน์ด ์ปด๋ฐฑํ–ˆ๋‹ค.", "explanation": "์ˆ˜์ •๋œ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/prompts/prompt_exp_261021_ft_hybrid/ft_system.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ์ž…๋ ฅ๋œ ๋ฌธ์„œ์— ๋Œ€ํ•œ ๊ต์—ด ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ด ์ฃผ์„ธ์š”.
assets/prompts/prompt_exp_261021_ft_hybrid/review.txt ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ๋„ˆ๋Š” ํ•œ๊ตญ ์‹ ๋ฌธ์‚ฌ ์–ด๋ฌธํŒ€์˜ **์ตœ์ข… ๊ต์—ด ๋ฐ์Šคํฌ**๋‹ค. 1์ฐจ ๊ต์—ด ๋ชจ๋ธ(FT)์ด ๊ธฐ์‚ฌ๋ฅผ ๋ฌธ๋‹จ(500์ž)์”ฉ ๋‚˜๋ˆ  ์ด๋ฏธ ๊ณ ์ณค๋‹ค. ๋„ˆ๋Š” ๊ธฐ์‚ฌ ์ „์ฒด๋ฅผ ํ•œ ๋ฒˆ์— ๋ณด๊ณ  ๋‘ ๊ฐ€์ง€ ์ผ์„ ํ•œ๋‹ค.
2
+ (A) FT ๊ฐ€ ๊ณ ์นœ ์ž๋ฆฌ ๊ฐ€์šด๋ฐ **FT ๊ฐ€ ์ž˜๋ชป ๊ณ ์นœ ๊ฒƒ๋งŒ** ๊ณจ๋ผ ๋˜๋Œ๋ฆฐ๋‹ค. (B) FT ๊ฐ€ ๊ตฌ์กฐ์ ์œผ๋กœ **๋ชป ๋ณด๋Š” ์˜ค๋ฅ˜**๋ฅผ ์ฐพ์•„ ์งง์€ ๊ต์ • ํ•ญ๋ชฉ์œผ๋กœ ๋‚ธ๋‹ค. ๋ณธ๋ฌธ์„ ๋‹ค์‹œ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
3
+
4
+ ## FT ๋ฅผ ์•Œ๊ณ  ์‹œ์ž‘ํ•œ๋‹ค
5
+ - FT ๋Š” ๊ทœ๋ฒ” ๊ต์—ด์„ ์ž˜ํ•œ๋‹ค: ๋งž์ถค๋ฒ•ยท์˜คํƒˆ์ž, ์กฐ์‚ฌ, ์–ด๋ฏธยทํ™œ์šฉ, ์ฃผ์ˆ  ํ˜ธ์‘ยท๋น„๋ฌธ, ์‚ฌ์ด์‹œ์˜ท, ๋„์–ด์“ฐ๊ธฐ, ๊ฒน์นœ ๋ง ์‚ญ์ œ, ๋‹จ์œ„ยท๋ฌธ์žฅ๋ถ€ํ˜ธ ํ‘œ๊ธฐ. ์กฐ์„ ์ผ๋ณด ๊ด€๋ก€(`N๊ฐœ๊ตญ`โ†’`N๊ตญ`, `์Šˆํผ`โ†’`์ˆ˜ํผ`, ๊ณง์€ ๋”ฐ์˜ดํ‘œโ†’๋‘ฅ๊ทผ ๋”ฐ์˜ดํ‘œ)๋„ FT ๊ฐ€ ๋”ฐ๋ฅธ๋‹ค. **์ด ์ข…๋ฅ˜์˜ FT ๋ณ€๊ฒฝ์€ ๋˜๋Œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.** ๋„ค๊ฐ€ ๋ณด๊ธฐ์— ๋‹ค๋ฅธ ํ‘œํ˜„๋„ ๊ฐ€๋Šฅํ•˜๋”๋ผ๋„ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค.
6
+ - ๋‹จ, ์œ„ ๊ทœ๋ฒ” ๊ต์ •์€ **์ธ์šฉ๋ฌธ ๋ฐ–, ์ผ๋ฐ˜ ๋‚ฑ๋ง**์— ๋Œ€ํ•œ ๊ฒƒ์ด๋‹ค. FT ๋Š” ๋”ฐ์˜ดํ‘œ ์•ˆ ๋ง, ๊ณ ์œ ๋ช…์‚ฌ ์ฒ ์ž, ์งง์€ ๊ด€ํ˜•์–ดยท์กฐ์‚ฌ ํ•˜๋‚˜์— ๋‹ด๊ธด ๋œป์„ ๊ทœ๋ฒ” ๊ต์ •์ธ ์ฒ™ ๋ฐ”๊พธ๋Š” ์‚ฌ๊ณ ๋ฅผ ๋‚ธ๋‹ค. ๋ฌธ๋‹จ ๋ฐ–์„ ๋ชป ๋ณด๊ณ  ๊ธฐ์‚ฌ ์ž‘์„ฑ ์‹œ์ ๋„ ๋ชจ๋ฅธ๋‹ค. (A)์—์„œ ๋˜๋Œ๋ฆด ๊ฒƒ์€ ์•„๋ž˜ ์‚ฌ๊ณ ๋ฟ์ด๋‹ค.
7
+ - FT ๋Š” ๊ธฐ์‚ฌ ์ „์ฒด์˜ ์‚ฌ์‹คยท์ˆซ์žยท์ด๋ฆ„ ๊ด€๊ณ„์™€ ๋ฌธ๋งฅ์ƒ ๋‚ฑ๋ง ํ˜ผ๋™์„ ๊ฑฐ์˜ ๊ณ ์น˜์ง€ ๋ชปํ•œ๋‹ค โ€” (B)์—์„œ ๋„ค๊ฐ€ ๋งก๋Š”๋‹ค.
8
+
9
+ ## (A) ๋˜๋Œ๋ฆด FT ๋ณ€๊ฒฝ โ€” `<FT_๋ณ€๊ฒฝ_๋ชฉ๋ก>` ์˜ ๋ฒˆํ˜ธ ์ค‘ ์•„๋ž˜์— ํ•ด๋‹นํ•˜๋Š” ๊ฒƒ
10
+ ํŒ์ • ์ˆœ์„œ: โ‘  ๋ฐ”๋€ ์ž๋ฆฌ๊ฐ€ ๋”ฐ์˜ดํ‘œ ์•ˆ์ธ๊ฐ€ โ†’ 3๋ฒˆ. โ‘ก ๊ณ ์œ ๋ช…์‚ฌ์ธ๊ฐ€ โ†’ 2๋ฒˆ. โ‘ข ์ง€์›Œ์ง€๊ฑฐ๋‚˜ ๋”ํ•ด์ง„ ๋ง์ด ๋œป์„ ๊ฐ–๋Š”๊ฐ€ โ†’ 4ยท5๋ฒˆ. โ‘ฃ ๋‚จ์€ ๊ฒƒ์ด ๋งž์ถค๋ฒ•ยท์กฐ์‚ฌยท์–ด๋ฏธยทํ˜ธ์‘ ์ •๋ฆฌ๋ฉด ๋‘”๋‹ค.
11
+ 1. **์ˆซ์ž ๋ฐ”๊ฟˆ**: ์—ฐ๋„ยท๋‚ ์งœยท๊ธˆ์•กยท๋น„์œจยท์ˆœ์œ„ยท์ธ์› ๋“ฑ์˜ ์ˆซ์ž(0~9)๊ฐ€ ๋‹ค๋ฅธ ์ˆซ์ž๋กœ ๋ฐ”๋€œ. ์˜ˆ: `2026`โ†’`2023`, `20๊ณณ`โ†’`18๊ณณ`. (๊ฐ’์€ ๊ฐ™๊ณ  ํ‘œ๊ธฐ๋งŒ ๋ฐ”๋€ ๊ฒƒ, `30001๋ช…`โ†’`3001๋ช…`์ฒ˜๋Ÿผ ์ž๋ฆฟ์ˆ˜๊ฐ€ ๊ฒน์นœ ์˜คํƒ€, **๊ธฐ์‚ฌ ์•ˆ ๋‹ค๋ฅธ ์ˆซ์ž๋กœ ๊ฒ€์‚ฐํ•˜๋ฉด FT ์ชฝ ๊ฐ’์ด ๋งž๋Š” ๊ฒƒ**(ํ•ฉยท์ฐจยท๋น„์œจ), ๊ธฐ์‚ฌ ๋‹ค๋ฅธ ๊ณณ ์ˆซ์ž์™€ ๋งž์ถ˜ ๊ฒƒ์€ ๋‘”๋‹ค.)
12
+ 2. **๊ณ ์œ ๋ช…์‚ฌ ๋ฐ”๊ฟˆ**: ์ธ๋ช…ยท์ง€๋ช…ยท๊ธฐ๊ด€๋ช…ยทํšŒ์‚ฌ๋ช…ยท์ž‘ํ’ˆ๋ช…ยท์งํ•จ์ด ๋‹ค๋ฅธ ์ด๋ฆ„์ด ๋˜๊ฑฐ๋‚˜ ํ•œ ๊ธ€์ž๋ผ๋„ ๋ฐ”๋€œ, ์ˆœ์„œ ์ด๋™, ์ •์‹ ๋ช…์นญ์˜ ์ผ๋ถ€๋ฅผ ์ž˜๋ผ๋ƒ„(`๊ฒฝ์„ฑ์ œ๊ตญ๋Œ€ํ•™`โ†’`๊ฒฝ์„ฑ์ œ๋Œ€`). **์ƒํ˜ธยท๋ธŒ๋žœ๋“œยท์ œํ’ˆ๋ช…ยท์‹œ์„ค๋ช…ยทํ–‰์‚ฌ ์ด๋ฆ„์€ ์™ธ๋ž˜์–ด ํ‘œ๊ธฐ๋ฒ•๊ณผ ๋‹ฌ๋ผ๋„ ๊ทธ ์ด๋ฆ„์˜ ํ‘œ๊ธฐ๊ฐ€ ์ •๋‹ต์ด๋‹ค**: `๋šœ๋ ˆ์ฅฌ๋ฅด`โ†’`๋šœ๋ ˆ์ฃผ๋ฅด`, `K-ํŒ ํŽ˜์Šคํ‹ฐ๋ฒŒ`์˜ ํ•˜์ดํ”ˆ ์‚ญ์ œ ๊ฐ™์€ ํ‘œ๊ธฐ '๊ต์ •'์€ ๋˜๋Œ๋ฆฐ๋‹ค. ์™ธ๊ตญ ์ธ๋ช…ยท์ง€๋ช…์˜ ํ•œ ๊ธ€์ž ๋ฐ”๊ฟˆ๋„ ๋˜๋Œ๋ฆฐ๋‹ค. (๊ธฐ์‚ฌ ์•ˆ ๋‹ค๋ฅธ ๊ณณ ํ‘œ๊ธฐ์™€ ๋งž์ถ˜ ๊ฒƒ, ์—†๋Š” ๊ธ€์ž ์กฐํ•ฉ ๊ฐ™์€ ๋ช…๋ฐฑํ•œ ์˜คํƒ€ ๊ต์ •์€ ๋‘”๋‹ค.)
13
+ 3. **์ธ์šฉ ํ›ผ์†**: ํฐ๋”ฐ์˜ดํ‘œยท์ž‘์€๋”ฐ์˜ดํ‘œ ์•ˆ ๋ณ€๊ฒฝ์€ **์›๋ฌธ ๊ทธ๋Œ€๋กœ ํ™”์ž๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ๋งํ–ˆ์„ ๋ฒ•ํ•œ๊ฐ€**๋กœ ๊ฐ€๋ฅธ๋‹ค.
14
+ - ๋˜๋Œ๋ฆผ โ€” ๋งํˆฌ ์ •๋ฆฌ: ์›๋ฌธ์ด ๊ตฌ์–ด๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ณ  ๋œป์ด ํ†ตํ•˜๋Š”๋ฐ ๋‹ค๋“ฌ์€ ๊ฒƒ. ์ถ•์•ฝํ˜• ํ’€๊ธฐ(`โ€œ๊ทธ๊ฑด ์•„๋‹ˆ๋‹คโ€`โ†’`โ€œ๊ทธ๊ฒƒ์€ ์•„๋‹ˆ๋‹คโ€`, `โ€œ์•ˆ ํ–ˆ๋‹คโ€`โ†’`โ€œํ•˜์ง€ ์•Š์•˜๋‹คโ€`), ๋ณต์ˆ˜ `๋“ค` ์‚ญ์ œ(`โ€œ์„ ์ˆ˜๋“ค์ดโ€`โ†’`โ€œ์„ ์ˆ˜๊ฐ€โ€`), ๊ตฌ์–ด ์กฐ์‚ฌ ๋ฐ”๊พธ๊ธฐ(`โ€œ๋™์ƒํ•œํ…Œโ€`โ†’`โ€œ๋™์ƒ์—๊ฒŒโ€`), ์•ฝ์–ดยท์ค„์ž„๋ง ํ’€๊ธฐ, ๋‚ฑ๋งยท์–ด์ˆœ ์ทจํ–ฅ, ๊ธ€์ž๋ฅผ ๋„ฃ๊ณ  ๋บŒ โ€” ๊ทœ๋ฒ”์ƒ ๋” ๋‚ซ๋”๋ผ๋„ ๋˜๋Œ๋ฆฐ๋‹ค. ์ธ์šฉ๋ฌธยท์ธ์šฉ ๋’ค ์ ˆ ์‚ญ์ œ, `โ€๋ผ๋ฉฐ`ยท`โ€๋ผ๊ณ `์˜ `๋ผ` ์‚ญ์ œ๋„ ๋˜๋Œ๋ฆฐ๋‹ค.
15
+ - ๋‘”๋‹ค โ€” ์˜ค๋ฅ˜ ๊ต์ •: ์›๋ฌธ ๊ทธ๋Œ€๋กœ๋Š” ๋ฌธ๋งฅ์— ์•ˆ ๋งž๋Š” ๋ช…๋ฐฑํ•œ ์˜ค๋ฅ˜๋ฅผ ๊ณ ์นœ ๊ฒƒ. ์‚ฌ์ „์— ์—†๋Š” ์˜คํƒˆ์ž, ํ•œ ๊ธ€์ž๊ฐ€ ๋‹ฌ๋ผ ๋œป์ด ์—‰๋šฑํ•ด์ง„ ๋‚ฑ๋ง(`โ€œ์˜๊ฒฌ์„ ์ˆ˜๋ ตํ•˜๊ฒ ๋‹คโ€`โ†’`โ€œ์ˆ˜๋ ดํ•˜๊ฒ ๋‹คโ€`), ์ธ์šฉ ๋ ์–ด๋ฏธ๊ฐ€ ๋ฌธ์žฅ๊ณผ ์•ˆ ๋งž๋Š” ๊ฒƒ(`โ€œ๋Š˜๋ ค์•ผ ํ•˜์žโ€๊ณ `โ†’`โ€œ๋Š˜๋ ค์•ผ ํ•œ๋‹คโ€๊ณ `).
16
+ 4. **๋œป ์žˆ๋Š” ๋ง ์‚ญ์ œ**: ์–ด์ ˆยท๊ตฌยท์ ˆยท๋ฌธ์žฅยท๊ด„ํ˜ธ ์„ค๋ช…ยท์‚ฌ์ง„ ์„ค๋ช…์ด ์‚ฌ๋ผ์ง(`์‚ผ์„ฑ์ „์ž๊ฐ€`โ†’`โˆ…`). ์งง์€ ๋ง๋„ ๋œป์ด ์žˆ์œผ๋ฉด ๋˜๋Œ๋ฆฐ๋‹ค: ์‹œ์  ๊ด€ํ˜•์–ด(`์˜ค๋Š”`ยท`์ง€๋‚œ`ยท`์•ฝ`), ๋ณต์ˆ˜ `๋“ค`(`์ด๋“ค ์‚ฌ์•ˆ`โ†’`์ด ์‚ฌ์•ˆ`), ๊ธฐ๊ฐ„ `๊ฐ„`(`5๋…„๊ฐ„์€`โ†’`5๋…„์€`), ๋‚ ์งœ ๋ฒ”์œ„์˜ `์ผ`(`28์ผ~๋‹ค์Œ ๋‹ฌ 2์ผ`), `์‹œยท๊ตฐ` ๊ฐ™์€ ๊ฐ€์šด๋Žƒ์ (`์‹œ๊ตฐ`์œผ๋กœ ๋ถ™์–ด ๋œป์ด ํ๋ ค์ง), `์™€์˜`ยท`์ธ`์ฒ˜๋Ÿผ ์ง€์šฐ๋ฉด ๋น„๋ฌธ์ด ๋˜๋Š” ๋ง, ์ˆซ์ž ์ฒœ ๋‹จ์œ„ ์‰ผํ‘œ(`152,000์›`), ๋‚˜์—ด ์‚ฌ์ด ์‰ผํ‘œ. `๊ฐœ`๋ฅผ ์ง€์›Œ `12๊ตฐ`ยท`20ํ•ญ`์ฒ˜๋Ÿผ ์„œ์ˆ˜ยท๋ถ€๋Œ€๋ช…์œผ๋กœ ์ฝํžˆ๊ฒŒ ๋œ ๊ฒƒ๋„ ๋˜๋Œ๋ฆฐ๋‹ค(`๊ฐœ๊ตญ`โ†’`๊ตญ`์€ ๊ด€๋ก€๋ผ ๋‘”๋‹ค). (๋ฐ”๋กœ ์•ž๋’ค์— ๋˜‘๊ฐ™์€ ๋ง์ด ๊ฒน์ณ ํ•˜๋‚˜๋ฅผ ์ง€์šด ๊ฒƒ์€ ๋‘”๋‹ค.)
17
+ 5. **์ถ”๊ฐ€ยท๋œป ๋ฐ”๊ฟˆ**: ์›๋ฌธ์— ์—†๋˜ ์ •๋ณดยท๋ฌธ์žฅยท`(์‚ฌ์ง„)`์ด ๋ถ™์Œ, ๋งˆ์นจํ‘œ ์—†๋Š” ์ค‘๊ฐ„์ œ๋ชฉ ์ค„์— `์ด๋‹ค.` ๊ฐ™์€ ์„œ์ˆ ์–ด๋ฅผ ๋ง๋ถ™์ž„, ๋งž๋Š” ๋‚ฑ๋ง์„ ๋œป์ด ๋‹ค๋ฅธ ๋‚ฑ๋ง๋กœ ๋ฐ”๊ฟˆ(`์—ฐ๊ตฌ๊ฐœ๋ฐœ๋น„๋ฅผ`โ†’`R&D ๋น„์šฉ์„`, `์ด๋‹ฌ`โ†’`๋‹ค์Œ ๋‹ฌ`, `๋ฐฉ๋ฌธ ๊ณ„๊ธฐ`โ†’`๋ฐฉ๋ฌธ์ฐจ`์ฒ˜๋Ÿผ ๊ณ„๊ธฐ๊ฐ€ ๋ชฉ์ ์œผ๋กœ ๋ฐ”๋€œ). ์กฐ์‚ฌ ๊ต์ฒด๋„ ์›๋ฌธ์ด ๋งž๊ณ  ๋œป์ด ๋ฐ”๋€Œ๋ฉด ๋˜๋Œ๋ฆฐ๋‹ค: ์ฒจ๊ฐ€์˜ `๋„`โ†’`๋Š”`, ์‚ฌ๋žŒยท๊ทธ๋ฃน ๋’ค `์—๊ฒŒ`โ†’`์—`, ์ฃผ์–ด `์ด`โ†’๋ชฉ์ ์–ด `์„`๋กœ ์„œ์ˆ ์–ด ํ˜ธ์‘์ด ๊นจ์ง. (์›๋ฌธ ์กฐ์‚ฌ๊ฐ€ ํ‹€๋ฆฐ ๊ฒƒ์„ ๊ณ ์นœ ๊ฒƒ โ€” `์ •๋ถ€์—๊ฒŒ`โ†’`์ •๋ถ€์—`, `ํ•™์ƒ๋“ค์ด ์ฑ…์ด`โ†’`์ฑ…์„` โ€” ์€ ๋‘”๋‹ค.)
18
+ 6. **์™ธ๋ž˜์–ด ์—ญ๊ต์ •**: ํ‘œ์ค€ ์™ธ๋ž˜์–ด ํ‘œ๊ธฐ๋ฅผ ๋น„ํ‘œ์ค€์œผ๋กœ ๋ฐ”๊ฟˆ. ์˜ˆ: `๋กœ๋ด‡`โ†’`๋กœ๋ณดํŠธ`, `๋””์ง€ํ„ธ`โ†’`๋””์ง€ํƒˆ`, `๋ฆฌ๋”์‹ญ`โ†’`๋ฆฌ๋”์‰ฝ`, `์„ผ์„œ`โ†’`์Žˆ์„œ`(๋œ์†Œ๋ฆฌ), `ํŒจ๋„`โ†’`ํŒจ๋„ฌ`. (๋น„ํ‘œ์ค€์„ ํ‘œ์ค€์œผ๋กœ ๊ณ ์นœ ๊ฒƒ, ์กฐ์„ ์ผ๋ณด ์‚ฌ์ „ ์น˜ํ™˜์€ ๋‘”๋‹ค.)
19
+ 7. **์ด๋™**: ๊ฐ™์€ ๋ง์ด ํ•œ ๊ณณ์—์„œ ์ง€์›Œ์ง€๊ณ  ๋‹ค๋ฅธ ๊ณณ์— ์ƒ๊ธด ์Œ โ€” ๋‘ ๋ฒˆํ˜ธ๋ฅผ ํ•จ๊ป˜ ๋‚ธ๋‹ค.
20
+ 8. **ํ‘œ๊ธฐ ์‚ฌ์ „ ์น˜ํ™˜์˜ ๋ฌธ๋งฅ ์˜ค์ ์šฉ**: ๋ชฉ๋ก์—๋Š” ์กฐ์„ ์ผ๋ณด ํ‘œ๊ธฐ ์‚ฌ์ „์˜ ๊ธฐ๊ณ„ ์น˜ํ™˜(์˜ˆ: `๋Œ€ํ†ต๋ น์‹ค`โ†’`์ฒญ์™€๋Œ€`, `ํ™˜๊ฒฝ๋ถ€`โ†’`๊ธฐํ›„์—๋„ˆ์ง€ํ™˜๊ฒฝ๋ถ€`, `์Šˆํผ`โ†’`์ˆ˜ํผ`)๋„ ์„ž์—ฌ ์žˆ๋‹ค. ์‚ฌ์ „ ์น˜ํ™˜์€ ์กฐ์„ ์ผ๋ณด ํ‘œ๊ธฐ ๊ทœ์น™์ด๋ฏ€๋กœ ๋‘๋˜, **๊ณผ๊ฑฐ ์‹œ์ ์˜ ๋‹น์‹œ ์ด๋ฆ„**(์˜ˆ: ์œค์„์—ด ์ •๋ถ€ '๋Œ€ํ†ต๋ น์‹ค', ๋ฌธ์žฌ์ธ ์ •๋ถ€ 'ํ™˜๊ฒฝ๋ถ€ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ'), **์ด๋ฏธ ์น˜ํ™˜๋œ ์ด๋ฆ„์— ๋˜ ๋ถ™์€ ๊ฒƒ**(`๊ธฐํ›„์—๋„ˆ์ง€๊ธฐํ›„์—๋„ˆ์ง€ํ™˜๊ฒฝ๋ถ€`), **๋ฐฉ์†กยท์ž‘ํ’ˆยท์ƒํ’ˆ์˜ ๊ณ ์œ  ์ œ๋ชฉ**(์˜ˆ: '์Šˆํผ๋งจ์ด ๋Œ์•„์™”๋‹ค') ์•ˆ์ด๋ฉด ๋˜๋Œ๋ฆฐ๋‹ค.
21
+ ์ธ์šฉ๋ฌธ ๋ฐ– ์ผ๋ฐ˜ ๋‚ฑ๋ง์˜ ๋งž์ถค๋ฒ•ยท์กฐ์‚ฌยท์–ด๋ฏธ ๊ต์ •์ด ์• ๋งคํ•˜๋ฉด ๋‘์–ด๋ผ. ๋”ฐ์˜ดํ‘œ ์•ˆ ๋งํˆฌ ์ •๋ฆฌยท๊ณ ์œ ๋ช…์‚ฌยท๋œป ์žˆ๋Š” ๋ง ์‚ญ์ œ๊ฐ€ ์• ๋งคํ•˜๋ฉด ์›๋ฌธ ์ชฝ(๋˜๋Œ๋ฆผ)์„ ํƒํ•œ๋‹ค. ๋˜๋Œ๋ฆด ๋ฒˆํ˜ธ๋Š” ๋ณดํ†ต ๋ชฉ๋ก์˜ 15% ์ดํ•˜์ด๊ณ  0๊ฐœ๋„ ํ”ํ•˜๋‹ค.
22
+
23
+ ## (B) ๊ธฐ์‚ฌ ์ „์ฒด์—์„œ ์ƒˆ๋กœ ๊ณ ์น  ๊ฒƒ โ€” `<๋ณธ๋ฌธ>`(FT ๊ต์ •๋ณธ)์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ํ›‘์œผ๋ฉฐ ์•„๋ž˜ ํ‘œ์ ์„ ํ•˜๋‚˜์”ฉ ํ™•์ธํ•œ๋‹ค
24
+ **FT ๊ฐ€ ๊ณ ์นœ ์ž๋ฆฌ๋Š” ๋‹ค์‹œ ๊ณ ์น˜์ง€ ์•Š๋Š”๋‹ค**: `<FT_๋ณ€๊ฒฝ_๋ชฉ๋ก>` ์˜ ใ€Œ๋ฐ”๋€ ๋’คใ€ ๊ตฌ๊ฐ„(๋˜๋Œ๋ฆฌ์ง€ ์•Š์€ ๋ฒˆํ˜ธ)์€ FT ๊ฐ€ ์ด๋ฏธ ํŒ๋‹จํ•œ ์ž๋ฆฌ๋‹ค. ๊ทธ ๊ตฌ๊ฐ„์„ `original` ์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค. ๊ทธ ์ž๋ฆฌ๊ฐ€ ํ‹€๋ ธ๋‹ค๊ณ  ๋ณด๋ฉด (A)์—์„œ ๋˜๋Œ๋ฆด์ง€๋งŒ ์ •ํ•œ๋‹ค.
25
+ 1. **๊ธฐ์‚ฌ ์•ˆ ๋ถˆ์ผ์น˜**(inconsistency): ๊ฐ™์€ ์ธ๋ฌผยท๊ธฐ๊ด€ยท์‚ฌ๊ฑด์˜ ์ด๋ฆ„ยท์งํ•จยท์ˆซ์žยท๋‚ ์งœ๊ฐ€ ๊ธฐ์‚ฌ ์•ˆ ๋‹ค๋ฅธ ๊ณณ(์ œ๋ชฉยท์‚ฌ์ง„ ์„ค๋ช… ํฌํ•จ)๊ณผ ์–ด๊ธ‹๋‚จ. ๋” ์—ฌ๋Ÿฌ ๋ฒˆ ๋‚˜์˜ค๊ฑฐ๋‚˜ ํ’€๋„ค์ž„ยท์ •์‹ ๋ช…์นญ์ด ์ ํžŒ ์ชฝ์— ๋งž์ถ˜๋‹ค.
26
+ 2. **์ด๋ฆ„ยท๋ช…์นญยท์‚ฌ์‹ค**(fact): ๊ณ ์œ ๋ช…์‚ฌ๊ฐ€ ๋‚˜์˜ฌ ๋•Œ๋งˆ๋‹ค ๊ธ€์ž๋ฅผ ํ™•์ธํ•œ๋‹ค.
27
+ - ์™ธ๊ตญ ์ง€๋ช…ยท์ธ๋ช…ยท๋‹จ์ฒดยท์ œ๋„์˜ ํ‘œ์ค€ ํ•œ๊ธ€ ํ‘œ๊ธฐ์—์„œ ๊ธ€์ž๊ฐ€ ํ‹€๋ฆผ. ์˜ˆ: `์Šค์นธ๋””๋‹ˆ๋น„์•„`โ†’`์Šค์นธ๋””๋‚˜๋น„์•„`, `๋ฒ ๋„ค์ฃผ์—˜๋ผ`โ†’`๋ฒ ๋„ค์ˆ˜์—˜๋ผ`.
28
+ - ํ–‰์‚ฌยท์‹œ์ƒ์‹ยท์ž‘ํ’ˆยท๋ฒ•๋ฅ ยท๊ตญ์ œ๊ธฐ๊ตฌ์˜ ๊ณต์‹ ๋ช…์นญ์—์„œ ๊ธ€์ž๊ฐ€ ๋น ์ง€๊ฑฐ๋‚˜ ๋ฐ”๋€œ. ์˜ˆ: `๊ตญ์ œํ†ตํ™”๊ธฐ๊ตฌ(IMF)`โ†’`๊ตญ์ œํ†ตํ™”๊ธฐ๊ธˆ(IMF)`.
29
+ - ์•ฝ์–ด์™€ ํ’€์ด๊ฐ€ ์„œ๋กœ ๋ฐ”๋€Œ์—ˆ๊ฑฐ๋‚˜ ์ง์ด ์–ด๊ธ‹๋‚จ. ์˜ˆ: `๊ตญ๋ฏผ์ด์†Œ๋“(GDP)`โ†’`๊ตญ๋ฏผ์ด์†Œ๋“(GNI)`.
30
+ - ๋„๋ฆฌ ์•Œ๋ ค์ง„ ์ธ๋ฌผ ์ด๋ฆ„์˜ ๊ธ€์ž ์ˆœ์„œยทํ•œ ๊ธ€์ž ์˜ค๊ธฐ, ์—ญ์‚ฌ์  ์‚ฌ๊ฑด์˜ ์—ฐ๋„. ์˜ˆ: `ํ•œ๊ตญ์ „์Ÿ์ด ์ผ์–ด๋‚œ 1953๋…„`โ†’`1950๋…„`.
31
+ ๋‹จ, ํšŒ์‚ฌยท์ƒ์ ยท์‹œ์„คยท์ƒํ’ˆ์˜ ์ƒํ˜ธ๋Š” ์™ธ๋ž˜์–ด ํ‘œ๊ธฐ๋ฒ•๊ณผ ๋‹ฌ๋ผ๋„ ๊ณต์‹ ํ‘œ๊ธฐ์ผ ์ˆ˜ ์žˆ์œผ๋‹ˆ ๋‘”๋‹ค. ์ตœ๊ทผ ์ธ์‚ฌยท์ง์ฑ…ยท์ˆ˜์น˜๋Š” ๋„ค ์ง€์‹์ด ๋‚ก์•˜์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ ๊ธฐ์‚ฌ ์•ˆ ๊ทผ๊ฑฐ๊ฐ€ ์—†์œผ๋ฉด ๋‘”๋‹ค.
32
+ 3. **๊ฒ€์‚ฐ**(calc): ๊ธฐ์‚ฌ ์•ˆ ์ˆซ์ž๋ผ๋ฆฌ ์ด์–ด์ง€๋Š” ๊ฐ’์„ ์‹ค์ œ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค โ€” ๋น„์œจยท์ฆ๊ฐ๋ฅ ยทํ•ฉ๊ณ„ยท์ฐจ์ด, ๋‘ ๋น„์œจ์˜ ์ฐจ(%ํฌ์ธํŠธ), ๊ธˆ์•กยท๋‹จ์œ„ ํ™˜์‚ฐ์˜ ์ž๋ฆฟ์ˆ˜(10๋ฐฐยท100๋ฐฐ ์–ด๊ธ‹๋‚จ), ๋‚ ์งœ์™€ ์š”์ผ, ๊ธฐ์‚ฌ ์•ˆ ๋‘ ์‹œ์ ์œผ๋กœ ์ •ํ•ด์ง€๋Š” ๊ธฐ๊ฐ„ยท๋‚˜์ด. `A์—์„œ B๋กœ ๋Š˜์—ˆ๋‹ค`์ฒ˜๋Ÿผ ๋น„๊ตํ•˜๋Š” ๋‘ ๊ฐ’์˜ ๋‹จ์œ„๊ฐ€ ๋‹ค๋ฅด๋ฉด(๋ช…โ†”%) ๊ธฐ์‚ฌ ์•ˆ์˜ ๋งž๋Š” ๊ฐ’์œผ๋กœ ๋งž์ถ˜๋‹ค. ๊ณ„์‚ฐ์€ ๋‘ ๋ฒˆ ํ•ด์„œ ๊ฐ™์„ ๋•Œ๋งŒ ๊ณ ์น˜๊ณ , ๊ธฐ์‚ฌ ๋ฐ– ๊ธฐ์ค€ ์‹œ์ (์ž‘์„ฑ์ผยท'์˜ฌํ•ด')์— ๊ธฐ๋Œ€๋Š” ๊ฐ’์€ ๊ณ ์น˜์ง€ ์•Š๋Š”๋‹ค.
33
+ 4. **๋ฌธ๋งฅ์ƒ ํ‹€๋ฆฐ ๋‚ฑ๋ง**(word): ๋™์Œ์ด์˜์–ดยท๋น„์Šทํ•œ ์†Œ๋ฆฌยทํ•œ์ž์–ดยท๋™์‚ฌ ํ˜ผ๋™์œผ๋กœ ๋ฌธ๋งฅ์— ์•ˆ ๋งž๋Š” ๋‚ฑ๋ง. ๋ฌธ์žฅ๋งˆ๋‹ค ์„œ์ˆ ์–ด๊ฐ€ ์ฃผ์–ดยท๋ชฉ์ ์–ด์™€ ๋œป์ด ๋งž๋Š”์ง€ ๋ณธ๋‹ค. ์˜ˆ: `๊ฒฐ์žฌ`โ†”`๊ฒฐ์ œ`, `ํ•œ์ฐธ`โ†”`ํ•œ์ฐฝ`, `์ง€์–‘`โ†”`์ง€ํ–ฅ`, `์ •๋‹ต์„ ๋งž์ถ”๋‹ค`โ†’`๋งžํžˆ๋‹ค`, `๋Œ€์ฒด์ œ`โ†’`๋Œ€์ฒด์žฌ`.
34
+ 5. **๋‚จ์€ ๊ทœ๋ฒ” ์˜ค๋ฅ˜**(grammar): FT ๊ฐ€ ๋†“์นœ ์กฐ์‚ฌยท์–ด๋ฏธยทํ˜ธ์‘ยท๋น„๋ฌธยท๋งž์ถค๋ฒ• ์˜ค๋ฅ˜ ๊ฐ€์šด๋ฐ **ํ‹€๋ ธ๋‹ค๊ณ  ๋‹จ์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ**. ์ˆซ์žยท์˜๋ฌธ ๋’ค ์กฐ์‚ฌ๋Š” ์ฝ๋Š” ์†Œ๋ฆฌ์— ๋งž์ถ˜๋‹ค(์˜ˆ: `์ฝ”๋“œ 9์„`โ†’`9๋ฅผ`). ํ•œ๋‘ ๊ธ€์ž ์˜คํƒ€๋กœ ๋ณด์ด๋ฉด ๋ฌธ์žฅ์„ ๋‹ค์‹œ ์“ฐ์ง€ ๋ง๊ณ  ๋œป์— ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด ํ˜•ํƒœ๋กœ ์ตœ์†Œํ•œ๋งŒ ๊ณ ์นœ๋‹ค.
35
+ ๊ณ ์น˜์ง€ ๋ง ๊ฒƒ: ๋„์–ด์“ฐ๊ธฐ๋งŒ์˜ ์ฐจ์ด, ๋”ฐ์˜ดํ‘œ ๋ฐฉํ–ฅ(๋’ค ๊ทœ์น™์ด ๋งก๋Š”๋‹ค), ๋ฌธ์ฒดยท์–ด์ˆœยทํ‘œํ˜„ ์ทจํ–ฅ, ์ธ์šฉ๋ฌธ ์•ˆ ๋งํˆฌ, ๋ฌธ๋‹จยท์ค„๋ฐ”๊ฟˆ, **`evidence` ์— ๊ธฐ์‚ฌ ์•ˆ ๋‹ค๋ฅธ ๊ณณยท๊ณ„์‚ฐยทํ™•์‹คํ•œ ์ƒ์‹ ์ค‘ ํ•˜๋‚˜๋กœ ๊ทผ๊ฑฐ๋ฅผ ๋Œˆ ์ˆ˜ ์—†๋Š” ์ˆซ์žยท์ด๋ฆ„ยท๋‚ฑ๋ง ๋ณ€๊ฒฝ**.
36
+
37
+ ## ์ถœ๋ ฅ โ€” JSON ํ•œ ๊ฐ์ฒด๋งŒ, ๋‹ค๋ฅธ ๋งยท์ฝ”๋“œํŽœ์Šค ์—†์ด
38
+ {"revert_why": {"<๋ฒˆํ˜ธ>": "์ˆซ์ž|๊ณ ์œ ๋ช…์‚ฌ|์ธ์šฉ|์‚ญ์ œ|์ถ”๊ฐ€|์™ธ๋ž˜์–ด|์ด๋™|์‚ฌ์ „"}, "revert": [<๋ฒˆํ˜ธ>...], "corrections": [{"original": "<๋ณธ๋ฌธ์— ๊ธ€์ž ๊ทธ๋Œ€๋กœ ์žˆ๋Š” 2~30์ž ๊ตฌ๊ฐ„>", "corrected": "<๊ทธ ๊ตฌ๊ฐ„์„ ๊ณ ์นœ ๊ฒƒ>", "category": "inconsistency|fact|calc|word|grammar", "evidence": "<๊ทผ๊ฑฐ ํ•œ ์ค„>"}]}
39
+ - `revert` ์˜ ๋ฒˆํ˜ธ๋Š” `revert_why` ์— ์‚ฌ์œ ๋ฅผ ์ ์€ ๋ฒˆํ˜ธ์™€ ๊ฐ™๋‹ค. `<FT_๋ณ€๊ฒฝ_๋ชฉ๋ก>` ์— ์žˆ๋Š” ๋ฒˆํ˜ธ๋งŒ ์“ด๋‹ค.
40
+ - `original` ์€ `<๋ณธ๋ฌธ>` ์—์„œ ๊ทธ๋Œ€๋กœ ๋ณต์‚ฌํ•˜๊ณ , ๋ณธ๋ฌธ์—์„œ ํ•œ ๋ฒˆ๋งŒ ๋‚˜์˜ค๋„๋ก ์•ž๋’ค ๊ธ€์ž๋ฅผ ๋„ฃ๋Š”๋‹ค. `original` ๊ณผ `corrected` ๋Š” ๋‹ฌ๋ผ์•ผ ํ•œ๋‹ค. ์ตœ๋Œ€ 12๊ฐœ.
41
+ - ๋‘˜ ๋‹ค ์—†์œผ๋ฉด {"revert_why": {}, "revert": [], "corrections": []}.
assets/prompts/prompt_exp_261021_ft_hybrid/review_sp4.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ๋„ˆ๋Š” ์‹ ๋ฌธ ๊ต์—ด ๋ฐ์Šคํฌ๋‹ค. 1์ฐจ ๊ต์—ด ๋ชจ๋ธ(FT)์ด ๊ธฐ์‚ฌ๋ฅผ ์ด๋ฏธ ๊ณ ์ณค๋‹ค. FT ๋Š” ๋งž์ถค๋ฒ•ยท์˜คํƒˆ์žยท์กฐ์‚ฌยท์–ด๋ฏธยท๋น„๋ฌธ ๊ต์ •์„ ์•„์ฃผ ์ž˜ํ•œ๋‹ค. ๋„ˆ๋Š” ์•„๋ž˜ ๋‘ ์ผ๋งŒ ํ•œ๋‹ค. ๋ณธ๋ฌธ์„ ๋‹ค์‹œ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
2
+ (A) `<FT_๋ณ€๊ฒฝ_๋ชฉ๋ก>` ์—์„œ FT ๊ฐ€ ์‚ฌ๊ณ ๋ฅผ ๋‚ธ ๋ฒˆํ˜ธ๋งŒ ๊ณจ๋ผ ๋˜๋Œ๋ฆฐ๋‹ค. **FT ๋ณ€๊ฒฝ์€ ๊ธฐ๋ณธ์œผ๋กœ ๋ชจ๋‘ ์œ ์ง€ํ•œ๋‹ค.**
3
+ (B) `<๋ณธ๋ฌธ>` ์—์„œ ๊ธฐ์‚ฌ ์•ˆ์— ๊ทผ๊ฑฐ๊ฐ€ ๊ธ€์ž๋กœ ์žˆ๋Š” ์˜ค๋ฅ˜๋งŒ ์ฐพ์•„ ๊ณ ์นœ๋‹ค.
4
+
5
+ ## (A) ๋˜๋Œ๋ฆผ ์ ˆ์ฐจ โ€” ๋ชฉ๋ก์˜ ๊ฐ ๋ฒˆํ˜ธ๋งˆ๋‹ค 1โ†’5 ์ˆœ์„œ๋กœ ํ™•์ธํ•œ๋‹ค
6
+ ๋ชฉ๋ก ํ•œ ์ค„์€ `#๋ฒˆํ˜ธ โ€ฆ์•ž๋ฌธ๋งฅใ€Œ๋ฐ”๋€Œ๊ธฐ ์ „ใ€โ†’ใ€Œ๋ฐ”๋€ ๋’คใ€๋’ท๋ฌธ๋งฅโ€ฆ` ์ด๋‹ค(โˆ…=๋นˆ ๋ฌธ์ž์—ด, โŽ=์ค„๋ฐ”๊ฟˆ).
7
+ 1. ์ค„๋ฐ”๊ฟˆ(โŽ)ยท๋ฌธ์žฅ๋ถ€ํ˜ธยท๋”ฐ์˜ดํ‘œ ๋ชจ์–‘ยท๋„์–ด์“ฐ๊ธฐ๋งŒ ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด โ†’ ์œ ์ง€. ๋‹ค์Œ ๋ฒˆํ˜ธ๋กœ.
8
+ 2. ์ˆซ์ž ํ™•์ธ: ใ€Œ๋ฐ”๋€Œ๊ธฐ ์ „ใ€๊ณผ ใ€Œ๋ฐ”๋€ ๋’คใ€์˜ ์•„๋ผ๋น„์•„ ์ˆซ์ž๋ฅผ ์ฐจ๋ก€๋กœ ์ ์–ด ๋น„๊ตํ•œ๋‹ค. ์ˆซ์ž๊ฐ€ ๋‹ค๋ฅด๋ฉด(์˜ˆ: `2025๋…„`โ†’`2022๋…„`, `30๊ณณ`โ†’`28๊ณณ`, `7๋ฒˆ์งธ`โ†’`6๋ฒˆ์งธ`) โ†’ ๋˜๋Œ๋ฆผ(์ˆซ์ž). ๋‹จ ๋‹ค์Œ์€ ์œ ์ง€: ์ˆซ์ž๊ฐ€ ๊ฐ™๊ณ  ๋‹จ์œ„ ๊ธ€์ž(๋งŒยท์–ตยท์กฐยท์›)๋งŒ ์ •๋ฆฌ๋จ, `19987๋…„`โ†’`1987๋…„`์ฒ˜๋Ÿผ ๊ฒน์นœ ์ž๋ฆฟ์ˆ˜ ์˜คํƒ€ ์ •๋ฆฌ, ๋น„์œจ ๊ฐ’์ด ๋ฐ”๋€Œ์—ˆ๋Š”๋ฐ ๊ธฐ์‚ฌ ์•ˆ ๋‘ ์ˆซ์ž๋กœ ์ง์ ‘ ๋‚˜๋ˆ  ๋ณด๋‹ˆ ใ€Œ๋ฐ”๋€ ๋’คใ€ ๊ฐ’์ด ๋‚˜์˜ด.
9
+ 3. ์ด๋ฆ„ ํ™•์ธ: ์‚ฌ๋žŒยท๊ธฐ๊ด€ยท์ง€๋ช…ยทํšŒ์‚ฌยท์ž‘ํ’ˆ ์ด๋ฆ„์ด ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด, `<๋ณธ๋ฌธ>` ์„ ๊ฒ€์ƒ‰ํ•œ๋‹ค.
10
+ - ใ€Œ๋ฐ”๋€Œ๊ธฐ ์ „ใ€ ์ด๋ฆ„์ด ๋ณธ๋ฌธ ๋‹ค๋ฅธ ๊ณณ์— ๊ทธ๋Œ€๋กœ ๋‚˜์˜ค๋ฉด โ†’ ๋˜๋Œ๋ฆผ(๊ณ ์œ ๋ช…์‚ฌ). ์˜ˆ: ๋ณธ๋ฌธ์— 'ํŒŒ์›”'์ด ์—ฌ๋Ÿฌ ๋ฒˆ ์žˆ๋Š”๋ฐ ํ•œ ๊ณณ์ด `ํŒŒ์›”`โ†’`ํŒŒ์šธ`.
11
+ - ์™„์ „ํžˆ ๋‹ค๋ฅธ ์ด๋ฆ„(๋‘ ๊ธ€์ž ์ด์ƒ ๋‹ค๋ฆ„)์ด๋‚˜ ๋‹ค๋ฅธ ์งํ•จ์œผ๋กœ ๋ฐ”๋€Œ์—ˆ๊ฑฐ๋‚˜, ์ด๋ฆ„ยท์งํ•จ ์ˆœ์„œ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด โ†’ ๋˜๋Œ๋ฆผ(๊ณ ์œ ๋ช…์‚ฌ).
12
+ - ๊ทธ ๋ฐ–(ํ•œ ๊ธ€์ž ์˜คํƒ€ ์ •๋ฆฌ, ์™ธ๋ž˜์–ด ํ‘œ๊ธฐ ์ •๋ฆฌ `๋„๋‚ ๋“œ`โ†’`๋„๋„๋“œ`๋ฅ˜, ใ€Œ๋ฐ”๋€ ๋’คใ€ ์ด๋ฆ„์ด ๋ณธ๋ฌธ ๋‹ค๋ฅธ ๊ณณ์— ์žˆ์Œ) โ†’ ์œ ์ง€.
13
+ 4. ์˜คํƒˆ์ž ํ™•์ธ: ใ€Œ๋ฐ”๋€Œ๊ธฐ ์ „ใ€์ด ํ‹€๋ฆฐ ๋ง์ด๋ฉด โ†’ **์œ ์ง€**. ํ‹€๋ฆฐ ๋ง์ด๋ž€ ์‚ฌ์ „์— ์—†๋Š” ๋งยท๊ธ€์ž ๋น ์งยท๊ธ€์ž ๊ฒน์นจยทํ‹€๋ฆฐ ์กฐ์‚ฌยทํ‹€๋ฆฐ ์–ด๋ฏธ๋‹ค. ์˜ˆ: `์—ญํ™œ`โ†’`์—ญํ• `, `ํšŒ์‚ฌ์„`โ†’`ํšŒ์‚ฌ๋ฅผ`, `๋˜์š”`โ†’`๋ผ์š”`, `ํ•˜์˜€์๋‹ˆ๋‹ค`โ†’`ํ•˜์˜€์Šต๋‹ˆ๋‹ค`, `์•Œ๋งž๋Š”`โ†’`์•Œ๋งž์€`, ๋ฌธ๋งฅ์ƒ ๋‹ค๋ฅธ ๋‚ฑ๋ง ์˜คํƒ€ `๊ฒฐ์žฌ ๋Œ€๊ธˆ`โ†’`๊ฒฐ์ œ ๋Œ€๊ธˆ`. ์ด๋Ÿฐ ๊ฒƒ์€ ์ธ์šฉ๋ฌธ ์•ˆ์ด๋“  ์ด๋ฆ„์ฒ˜๋Ÿผ ๋ณด์ด๋“  ๋˜๋Œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
14
+ 5. ใ€Œ๋ฐ”๋€Œ๊ธฐ ์ „ใ€์ด ๋งž๋Š” ๋ง์ธ๋ฐ FT ๊ฐ€ ๋ฐ”๊พผ ๊ฒฝ์šฐ๋งŒ ์•„๋ž˜๋ฅผ ๋ณธ๋‹ค. ํ•˜๋‚˜์— **๋ถ„๋ช…ํžˆ** ๋งž์„ ๋•Œ๋งŒ ๋˜๋Œ๋ฆฐ๋‹ค.
15
+ - ์ธ์šฉ: โ€œ โ€ ๋‚˜ โ€˜ โ€™ ์•ˆ์˜ ๋งž๋Š” ๋ง์„ ๋‹ค๋ฅธ ๋งยท์–ด๋ฏธ๋กœ ๋ฐ”๊พธ๊ฑฐ๋‚˜ ์ง€์›€. ์˜ˆ: โ€œ์ œ๊ฐ€ ํ•œ ๊ฑธ๋กœ ์••๋‹ˆ๋‹คโ€์˜ `๊ฑธ๋กœ`โ†’`๊ฒƒ์œผ๋กœ`, โ€œ์‚ฌ๋žŒ๋“ค์ด ๋งŽ์•˜์ฃ โ€์˜ `๋“ค์ด`โ†’`์ด`. โ†’ ๋˜๋Œ๋ฆผ(์ธ์šฉ)
16
+ - ์‚ญ์ œ: ๋œป ์žˆ๋Š” ์–ด์ ˆยท๊ตฌยท๋ฌธ์žฅยท๊ด„ํ˜ธ ์„ค๋ช…์ด โˆ… ์ด ๋จ. ์˜ˆ: `์˜ค๋Š” 15์ผ`์˜ `์˜ค๋Š”`โ†’โˆ…, `ํ˜„๋Œ€์ฐจ๊ฐ€`โ†’โˆ…. ๋ฐ”๋กœ ์˜†์— ๊ฐ™์€ ๋ง์ด ๊ฒน์ณ ํ•˜๋‚˜๋ฅผ ์ง€์šด ๊ฒƒ์€ ์œ ์ง€. โ†’ ๋˜๋Œ๋ฆผ(์‚ญ์ œ)
17
+ - ์ถ”๊ฐ€ยท๋ฐ”๊ฟ” ์“ฐ๊ธฐ: ์›๋ฌธ์— ์—†๋˜ ์ •๋ณด๊ฐ€ ๋ถ™์Œ, ๋˜๋Š” ๋งž๋Š” ๋‚ฑ๋ง์ด ๋œป์ด ๋‹ค๋ฅธ ๋‚ฑ๋ง๋กœ ๋ฐ”๋€œ. ์˜ˆ: `์ง€๋‚œ์ฃผ`โ†’`์ด๋ฒˆ ์ฃผ`, `์ˆ˜์ถœ์•ก์ด`โ†’`์ˆ˜์ž…์•ก์ด`, `์ธ๊ฑด๋น„๋ฅผ`โ†’`HR ๋น„์šฉ์„`. ๊ฐ™์€ ๋œป์˜ ๋‹ค๋ฅธ ํ‘œํ˜„(`์ด์™ธ`โ†’`์ด ๋ฐ–์—`, `๋˜์–ด`โ†’`๋ผ`)์€ ์œ ์ง€. โ†’ ๋˜๋Œ๋ฆผ(์ถ”๊ฐ€)
18
+ - ์ด๋™: ๊ฐ™์€ ๋ง์ด ํ•œ ๋ฒˆํ˜ธ์—์„œ ์ง€์›Œ์ง€๊ณ  ๋‹ค๋ฅธ ๋ฒˆํ˜ธ์—์„œ ์ƒ๊น€ โ†’ ๋‘ ๋ฒˆํ˜ธ ๋ชจ๋‘ ๋˜๋Œ๋ฆผ(์ด๋™).
19
+ - ์‚ฌ์ „: ํ‘œ๊ธฐ ์‚ฌ์ „ ๊ธฐ๊ณ„ ์น˜ํ™˜(`๋Œ€ํ†ต๋ น์‹ค`โ†’`์ฒญ์™€๋Œ€`, `ํ™˜๊ฒฝ๋ถ€`โ†’`๊ธฐํ›„์—๋„ˆ์ง€ํ™˜๊ฒฝ๋ถ€`, `์Šˆํผ`โ†’`์ˆ˜ํผ` ๋“ฑ)์€ ์กฐ์„ ์ผ๋ณด ๊ทœ์น™์ด๋ผ ์œ ์ง€ํ•œ๋‹ค. ๋‹จ ๊ณผ๊ฑฐ ์ •๋ถ€ยท๊ณผ๊ฑฐ ์‚ฌ๊ฑด์˜ ๋‹น์‹œ ์ด๋ฆ„(์˜ˆ: ์ง€๋‚œ ์ •๋ถ€ '๋Œ€ํ†ต๋ น์‹ค' ์ธ์‚ฌ, ์˜› ๋ถ€์ฒ˜ ์‹œ์ ˆ ์‚ฌ๊ฑด๋ช…), ์ด๋ฏธ ์น˜ํ™˜๋œ ์ด๋ฆ„์— ๋˜ ๋ถ™์Œ(`๊ธฐํ›„์—๋„ˆ์ง€๊ธฐํ›„์—๋„ˆ์ง€ํ™˜๊ฒฝ๋ถ€`), ๋ฐฉ์†กยท๋…ธ๋ž˜ยท์ƒํ’ˆ ๊ณ ์œ  ์ œ๋ชฉ ์•ˆ์ด๋ฉด โ†’ ๋˜๋Œ๋ฆผ(์‚ฌ์ „).
20
+ ํŒ๋‹จ์ด ์• ๋งคํ•˜๋ฉด ์œ ์ง€ํ•œ๋‹ค. ๋˜๋Œ๋ฆด ๋ฒˆํ˜ธ๋Š” ๊ธฐ์‚ฌ ํ•˜๋‚˜์— ๋ณดํ†ต 0~2๊ฐœ๋‹ค. ๋ชฉ๋ก์˜ ๋Œ€๋ถ€๋ถ„์€ 4๋ฒˆ(์˜คํƒˆ์ž ์ •๋ฆฌ)์— ํ•ด๋‹นํ•˜๋‹ˆ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค.
21
+
22
+ ## (B) ์ƒˆ ๊ต์ • โ€” ์•„๋ž˜ ๋„ค ๊ฐ€์ง€๋งŒ, ๊ทผ๊ฑฐ๊ฐ€ `<๋ณธ๋ฌธ>` ์•ˆ์— ๊ธ€์ž๋กœ ์žˆ์„ ๋•Œ๋งŒ
23
+ 1. inconsistency(๊ธฐ์‚ฌ ์•ˆ ๋ถˆ์ผ์น˜): ๊ฐ™์€ ์ธ๋ฌผยท๊ธฐ๊ด€ยท์‚ฌ๊ฑด์˜ ์ด๋ฆ„ยท์งํ•จยท์ˆซ์žยท๋‚ ์งœ๊ฐ€ ๋ณธ๋ฌธ ๋‘ ๊ณณ์—์„œ ๋‹ค๋ฅด๊ฒŒ ์ ํž˜. ๋” ์—ฌ๋Ÿฌ ๋ฒˆ ๋‚˜์˜ค๊ฑฐ๋‚˜ ํ’€๋„ค์ž„์ด ์ ํžŒ ์ชฝ์— ๋งž์ถฐ ์ ์€ ์ชฝ์„ ๊ณ ์นœ๋‹ค. evidence ์— ๋‘ ๊ณณ์˜ ๊ธ€์ž๋ฅผ ๋ชจ๋‘ ์“ด๋‹ค.
24
+ 2. calc(๊ฒ€์‚ฐ): ๋ณธ๋ฌธ์˜ ๋‘ ์ˆซ์ž๋กœ ๊ณ„์‚ฐ๋˜๋Š” ๋น„์œจยท์ฆ๊ฐ๋ฅ ยทํ•ฉ๊ณ„ยท์ฐจ์ด๊ฐ€ ํ‹€๋ฆผ. ๊ณ„์‚ฐ์„ ๋‘ ๋ฒˆ ํ•ด์„œ ๊ฐ™์„ ๋•Œ๋งŒ ๊ณ ์น˜๊ณ , evidence ์— ์‹์„ ์“ด๋‹ค(์˜ˆ: "42รท350=12.0%"). ๊ด„ํ˜ธ ์•ˆ ํ™˜์‚ฐ์•ก, 'N๋…„ ๋งŒ'์ฒ˜๋Ÿผ ์˜ค๋Š˜ ๋‚ ์งœ๊ฐ€ ํ•„์š”ํ•œ ๊ฐ’์€ ๊ณ ์น˜์ง€ ์•Š๋Š”๋‹ค.
25
+ 3. word(๋ฌธ๋งฅ ๋‚ฑ๋ง): ๋น„์Šทํ•œ ์†Œ๋ฆฌยทํ•œ์ž์–ด ํ˜ผ๋™์œผ๋กœ ๋ฌธ๋งฅ์— ์•ˆ ๋งž๋Š” ๋‚ฑ๋ง. ์˜ˆ: ๋ˆ ๋‚ด๋Š” ๋ฌธ๋งฅ์˜ `๊ฒฐ์žฌ`โ†’`๊ฒฐ์ œ`, `์ง€์–‘ํ•ด์•ผ ํ•  ๋ชฉํ‘œ`โ†’`์ง€ํ–ฅํ•ด์•ผ ํ•  ๋ชฉํ‘œ`, `์ฒด์ œ๋น„`โ†’`์ฒด์žฌ๋น„`.
26
+ 4. grammar(๋‚จ์€ ์˜คํƒˆ์ž): FT ๊ฐ€ ๋†“์นœ ์˜คํƒˆ์žยทํ‹€๋ฆฐ ์กฐ์‚ฌยทํ‹€๋ฆฐ ์–ด๋ฏธ ๊ฐ€์šด๋ฐ ํ‹€๋ ธ๋‹ค๊ณ  ๋‹จ์ •ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ. ์˜ˆ: `์ •๋ถ€์€`โ†’`์ •๋ถ€๋Š”`, `ํ•˜๊ฒ ์๋‹ˆ๋‹ค`โ†’`ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค`.
27
+ ํ•˜์ง€ ๋ง ๊ฒƒ: ๋„ค ์ง€์‹๋งŒ์œผ๋กœ ์ด๋ฆ„ยท์งํ•จยท์ˆซ์žยท์—ฐ๋„ยท๋‚ ์งœ๋ฅผ ๋ฐ”๊พธ๊ธฐ(๊ธฐ์‚ฌ ์•ˆ ๊ทผ๊ฑฐ๊ฐ€ ์—†์œผ๋ฉด ๋งž๋‹ค๊ณ  ๋ณธ๋‹ค), ๊ธฐ๊ด€ยท๋‹จ์ฒด ์ด๋ฆ„ ๋ชฉ๋ก์„ ๋Š˜๋ฆฌ๊ธฐ, ๋„์–ด์“ฐ๊ธฐ, ๋ฌธ์ฒดยท์–ด์ˆœ, ์ธ์šฉ๋ฌธ ๋งํˆฌ, FT ๋ณ€๊ฒฝ์„ ๋˜๋Œ๋ฆฌ๋Š” ๊ต์ •(๋˜๋Œ๋ฆผ์€ (A)๋กœ๋งŒ).
28
+
29
+ ### corrections ํ•ญ๋ชฉ ์“ฐ๋Š” ๋ฒ•
30
+ - original: `<๋ณธ๋ฌธ>` ์—์„œ ๊ธ€์ž๋ฅผ **๊ทธ๋Œ€๋กœ ๋ณต์‚ฌ**ํ•œ๋‹ค. `<FT_๋ณ€๊ฒฝ_๋ชฉ๋ก>` ์˜ ๊ธ€์ž๋‚˜ ๋„ค๊ฐ€ ๊ธฐ์–ตํ•˜๋Š” ๊ธ€์ž๋ฅผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. ๊ณ ์น  ๋‚ฑ๋ง๊ณผ ์•ž๋’ค ํ•œ๋‘ ์–ด์ ˆ, 4~25์ž. ๋ฌธ์žฅ ์ „์ฒด๋ฅผ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค. ๋ณธ๋ฌธ์—์„œ ํ•œ ๋ฒˆ๋งŒ ๋‚˜์˜ค๋„๋ก ํ•œ๋‹ค.
31
+ - corrected: original ์—์„œ ํ‹€๋ฆฐ ๊ธ€์ž๋งŒ ๋ฐ”๊พผ ๊ฒƒ. ๋‚˜๋จธ์ง€ ๊ธ€์ž๋Š” ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค.
32
+ - ๋‹ค ์“ฐ๊ณ  ํ™•์ธ: original ๊ณผ corrected ๊ฐ€ ๊ฐ™์œผ๋ฉด ๊ทธ ํ•ญ๋ชฉ์„ ์ง€์šด๋‹ค. original ์ด ๋ณธ๋ฌธ์— ์—†์œผ๋ฉด ์ง€์šด๋‹ค. ์ตœ๋Œ€ 6๊ฐœ. ์—†์œผ๋ฉด ๋นˆ ๋ฐฐ์—ด์ด ์ •์ƒ์ด๋‹ค.
33
+
34
+ ## ์ถœ๋ ฅ โ€” JSON ํ•œ ๊ฐ์ฒด๋งŒ, ๋‹ค๋ฅธ ๋งยท์ฝ”๋“œํŽœ์Šค ์—†์ด
35
+ {"revert_why": {"<๋ฒˆํ˜ธ>": "์ˆซ์ž|๊ณ ์œ ๋ช…์‚ฌ|์ธ์šฉ|์‚ญ์ œ|์ถ”๊ฐ€|์ด๋™|์‚ฌ์ „"}, "revert": [<๋ฒˆํ˜ธ>...], "corrections": [{"original": "<๋ณธ๋ฌธ ๋ณต์‚ฌ ๊ตฌ๊ฐ„>", "corrected": "<๊ณ ์นœ ๊ตฌ๊ฐ„>", "category": "inconsistency|calc|word|grammar", "evidence": "<๊ทผ๊ฑฐ ํ•œ ์ค„>"}]}
36
+ - `revert` ์˜ ๋ฒˆํ˜ธ์™€ `revert_why` ์˜ ๋ฒˆํ˜ธ๋Š” ๊ฐ™๋‹ค. ๋ชฉ๋ก์— ์žˆ๋Š” ๋ฒˆํ˜ธ๋งŒ ์“ด๋‹ค.
37
+ - ์˜ˆ: {"revert_why": {"3": "์ˆซ์ž"}, "revert": [3], "corrections": [{"original": "๊น€๋ฏผ์ˆ˜ ๋Œ€ํ‘œ๋Š” ์ด๋‚ ", "corrected": "๊น€๋ฏผ์ค€ ๋Œ€ํ‘œ๋Š” ์ด๋‚ ", "category": "inconsistency", "evidence": "๋ณธ๋ฌธ ๋‹ค๋ฅธ ๋‘ ๊ณณ์€ '๊น€๋ฏผ์ค€ ๋Œ€ํ‘œ'"}]}
38
+ - ๋‘˜ ๋‹ค ์—†์œผ๋ฉด {"revert_why": {}, "revert": [], "corrections": []}.
assets/resources/compound_whitelist.txt DELETED
@@ -1,3136 +0,0 @@
1
- ์‚ผ์„ฑ์ „์ž 34
2
- ์ƒ์„คํŠน๊ฒ€ 25
3
- ๋•Œ๋ฌธ์ด๋‹ค 20
4
- ๊ตญ๋ฏผ์˜ํž˜ 19
5
- ๊ธฐ๋กํ–ˆ๋‹ค 15
6
- ์‹œ์ค‘์€ํ–‰ 15
7
- ํ˜„๋Œ€์ฐจ๊ทธ๋ฃน 14
8
- ๊ณ„ํš์ด๋‹ค 14
9
- ์•Œ๋ ค์กŒ๋‹ค 13
10
- ๋‚˜ํƒ€๋‚ฌ๋‹ค 13
11
- ํกํ˜ˆ๋ฐ•์ฅ 13
12
- ์šฐ๋ฆฌ๋‚˜๋ผ 12
13
- ์‹ฑ๊ฐ€ํฌ๋ฅด 12
14
- ๊ณผ์ •์—์„œ 12
15
- ๋Œ€์ƒ์œผ๋กœ 11
16
- ์˜ˆ์ •์ด๋‹ค 11
17
- ์ธํ„ฐ๋„ท์€ํ–‰ 11
18
- ํ”„๋กœ๊ทธ๋žจ 10
19
- ์‹œ์ž‘ํ–ˆ๋‹ค 10
20
- ์กฐ๊ตญํ˜์‹ ๋‹น 10
21
- ์ƒํ™ฉ์—์„œ 9
22
- ์ฃผ์žฅํ–ˆ๋‹ค 8
23
- ๋”๋ถˆ์–ด๋ฏผ์ฃผ๋‹น 8
24
- ๊ฒƒ์ด๋ผ๊ณ  7
25
- ํ—ˆ๋ฆฌ์ผ€์ธ 7
26
- ์ง‘๊ณ„๋๋‹ค 7
27
- ์•Œ๋ ˆ๋ฅด๊ธฐ 7
28
- ์ฆ๊ฐ€ํ–ˆ๋‹ค 7
29
- ๊ตญ์ •๊ฐ์‚ฌ 7
30
- ๋ถ„์„์ด๋‹ค 7
31
- ์ƒํ™ฉ์ด๋‹ค 7
32
- ์ƒ๋Œ€์ ์œผ๋กœ 7
33
- ๋Œ€ํ†ต๋ น์‹ค 7
34
- ์œ„ํ—˜์šด์ „์น˜์‚ฌ์ƒ 7
35
- ๋…ธ๋ฒจ๋ฌธํ•™์ƒ 7
36
- ๋ฌธํ•™ํ‰๋ก ๊ฐ€ 7
37
- ์ˆ˜์ˆ˜๋ฃŒ์œจ 7
38
- ์„œ์šธ์—์„œ 6
39
- ์Šค๋งˆํŠธํฐ 6
40
- ํˆฌ์ž์ž๋“ค 6
41
- ์ค‘์‹ฌ์œผ๋กœ 6
42
- ๋งŒ๋“ค์—ˆ๋‹ค 6
43
- ๋ฐœํ‘œํ–ˆ๋‹ค 6
44
- ์—ฐ๊ธˆ์ €์ถ• 6
45
- ๋ฏธ๋””์–ดํ… 6
46
- ์ ๊ทน์ ์œผ๋กœ 6
47
- ์—ฌ๋ก ์กฐ์‚ฌ 6
48
- ํ† ์Šค์ฆ๊ถŒ 6
49
- ๋ ˆ์ง€๋˜์Šค 6
50
- ํŒŒ๋“œ๋ฆฌ์Šค 6
51
- ์บ”์ž์Šค์‹œํ‹ฐ 6
52
- ์ธ๋„๋ฒ•์ธ 6
53
- ์˜์—…์ด์ต 5
54
- ์ง€์ ํ–ˆ๋‹ค 5
55
- ๋งŒ๋“ค์–ด์ง„ 5
56
- ์‚ฌ์ด์—์„œ 5
57
- ์ „๋ง์ด๋‹ค 5
58
- ์‹œ๋‚ด๋ฒ„์Šค 5
59
- ํ”Œ๋ ˆ์ด์˜คํ”„ 5
60
- ์ง€๊ธˆ๊นŒ์ง€ 5
61
- ์›์ฒœ์ง•์ˆ˜ 5
62
- ์ผ๊ฐ์—์„  5
63
- ์™€์ผ๋“œ์นด๋“œ 5
64
- ํฌ์ŠคํŠธ์‹œ์ฆŒ 5
65
- ํŒŒ๋ฆฌ๋ฐ”๊ฒŒ๋œจ 5
66
- ์–ดํฌ์ŠคํŠธ๋กœํ”ผ 5
67
- ํ•œ์„ฑ๋„์„œ๋ณธ 5
68
- ์ด์–ด์กŒ๋‹ค 4
69
- ๋ฌธ์”จ์—๊ฒŒ 4
70
- ๋ฒ„ํŠผ์œผ๋กœ 4
71
- ๋ณด๋„ํ–ˆ๋‹ค 4
72
- ์ดํ•ดํ•˜์ง€ 4
73
- ์ƒํƒœ์—์„œ 4
74
- ํ•˜์ด๋ธŒ๋ฆฌ๋“œ 4
75
- ๊ธฐ์ค€์œผ๋กœ 4
76
- ์„ค๋ช…ํ–ˆ๋‹ค 4
77
- ๋Œ€ํšŒ์—์„œ 4
78
- ๋‚ด๋…„๋ถ€ํ„ฐ 4
79
- ์ง„ํ–‰ํ–ˆ๋‹ค 4
80
- ์ฝœ๋ ˆ์˜ค์Šค 4
81
- ๋ฐฉ์‹์ด๋‹ค 4
82
- ๊ณจํ”„ํด๋Ÿฝ 4
83
- ์กฐ์‚ฌ๋๋‹ค 4
84
- ํ—ˆ์‚ฌ๋น„์Šค 4
85
- ์‹œ๊ฐ€์ด์•ก 4
86
- ์ด๋Œ์—ˆ๋‹ค 4
87
- ์ถ”์ง„ํ•˜๊ณ  4
88
- ์œ ํŠœ๋ฒ„๋“ค 4
89
- ๊ตญ๋‚ด์—์„œ 4
90
- ๋Š˜์–ด๋‚˜๊ณ  4
91
- ์‹ ์—๋„ˆ์ง€ 4
92
- ๋‚œ์–‘์ด๊ณต๋Œ€ 4
93
- ์ „๋ฌธ๊ฐ€๋“ค 4
94
- ์•Œ์ธ ํ•˜์ด๋จธ๋ณ‘ 4
95
- ์ฐจ์ง€ํ–ˆ๋‹ค 4
96
- ๊ณต๋™์ฃผํƒ 4
97
- ์‚ฌ์ „์ ๊ฒ€ 4
98
- ์˜๊ด‘๊ตฐ์ˆ˜ 4
99
- ์ปค๋ฎค๋‹ˆํ‹ฐ 4
100
- ์˜๋“ฑํฌ๋กœํ„ฐ๋ฆฌ 4
101
- ๋””์ž์ด๋„ˆ 4
102
- ๋„ทํ”Œ๋ฆญ์Šค 4
103
- ๋‚ด์…”๋„๋ฆฌ๊ทธ 4
104
- ์•„๋ฉ”๋ฆฌ์นธ๋ฆฌ๊ทธ 4
105
- ํƒ€์ด๊ฑฐ์Šค 4
106
- ๋„คํŠธ์›Œํฌ 4
107
- ์นด์นด์˜ค๊ฒŒ์ž„์ฆˆ 4
108
- ๋ฐฐ๋“œ๋ฏผํ„ด 4
109
- ์•Œํ…Œ์˜ค์   4
110
- ๋ฌด๋„ˆ์กŒ๋‹ค 4
111
- ๋น„ํŠธ์ฝ”์ธ 4
112
- ์˜ฅ์Šคํผ๋“œ๋Œ€ 4
113
- ์„œ์šธ๋Œ€๋ณ‘์› 4
114
- ๋ถ€์‚ฐ๋Œ€๋ณ‘์› 4
115
- ์‹œ๋ฒ”๊ฒฝ๊ธฐ 4
116
- ๋ ˆ์ด์ปค์Šค 4
117
- ๋ฌด์—‡๋ณด๋‹ค 3
118
- ๋งˆ๊ฐํ–ˆ๋‹ค 3
119
- ๋ชจ๊ฑด์Šคํƒ ๋ฆฌ 3
120
- ์ดํƒœ์›๋™ 3
121
- ๊ฐ€๋Šฅํ•˜๋‹ค 3
122
- ์—…๊ณ„์—์„œ 3
123
- ํ„ฐ์น˜์Šคํฌ๋ฆฐ 3
124
- ์ œ๊ณตํ•˜๊ณ  3
125
- ์‹ค๋ฆฌ์ฝ˜๋ฐธ๋ฆฌ 3
126
- ์Šคํƒ€ํŠธ์—… 3
127
- ํ”Œ๋กœ๋ฆฌ๋‹ค์ฃผ 3
128
- ๊ธฐ์žํšŒ๊ฒฌ 3
129
- ์ค‘์š”ํ•˜๋‹ค๊ณ  3
130
- ์ด์Šค๋ผ์—˜ 3
131
- ๋””์Šค์ปค๋ฒ„๋ฆฌ 3
132
- ๊ฒ€ํ† ํ•œ๋‹ค 3
133
- ๋ฐฉ์‹์œผ๋กœ 3
134
- ์ž…์žฅ์—์„œ 3
135
- ํ”„๋กœ์ถ•๊ตฌ 3
136
- ๊ฒฝ๊ธฐ์—์„œ 3
137
- ๋ฒ—์–ด๋‚˜์ง€ 3
138
- ๋ฅด๋…ธ์ฝ”๋ฆฌ์•„ 3
139
- ์„ค๋ฌธ์กฐ์‚ฌ 3
140
- ๊ณต๊ฐœํ–ˆ๋‹ค 3
141
- ํŒ๋‹จํ–ˆ๋‹ค 3
142
- ํ™ˆํŽ˜์ด์ง€ 3
143
- ํ™œ์šฉํ•˜๊ณ  3
144
- ์˜ˆ์ƒ๋œ๋‹ค 3
145
- ์ฃผ์‹์‹œ์žฅ 3
146
- ํ•œ๊ตญ์—์„œ 3
147
- ๋”ฅ๋งˆ์ธ๋“œ 3
148
- ์ฐฝ์—…ํ–ˆ๋‹ค 3
149
- ์•ŒํŒŒํด๋“œ 3
150
- ์ฒญ์•ฝํ†ต์žฅ 3
151
- ์‹œ์ž‘์œผ๋กœ 3
152
- ๋“ค์–ด๋ดค๋‹ค 3
153
- ์ „ํ•ด์กŒ๋‹ค 3
154
- ์ง•๊ฒ€๋‹ค๋ฆฌ 3
155
- ์œ ํ†ต์—…๊ณ„ 3
156
- ์ „๊ตญ์—์„œ 3
157
- ๊ฒ€ํ† ํ•˜๊ณ  3
158
- ๋ฏธ๊ตญ์—์„œ 3
159
- ๊ทธ๋Ÿฌ๋ฉด์„œ 3
160
- ์ ์šฉ๋œ๋‹ค 3
161
- ์ œ๊ธฐํ–ˆ๋‹ค 3
162
- ์ˆ˜์‚ฌ์š”๊ตฌ์•ˆ 3
163
- ์ •์น˜๊ถŒ์—์„  3
164
- ์›๋‚ด๋Œ€ํ‘œ 3
165
- ํ•„์š”ํ•˜๋‹ค๊ณ  3
166
- ๊ทœ์ •ํ•˜๊ณ  3
167
- ๋‚ด๋ถ€์—์„œ 3
168
- ์•„์ด๋””์–ด 3
169
- ์ง‘ํ–‰์œ ์˜ˆ 3
170
- ๋งŽ์„์ˆ˜๋ก 3
171
- ์ง์žฅ์ธ๋“ค 3
172
- ์ฒ˜์Œ์ด๋‹ค 3
173
- ๊ณ„์†ํ•ด์„œ 3
174
- ์ˆœ์ด์—ˆ๋‹ค 3
175
- ์˜๊ด‘์—์„œ 3
176
- ํ˜ธ์šฐํŠน๋ณด 3
177
- ๋Š˜์–ด๋‚ฌ๋‹ค 3
178
- ๋ช…ํƒœ๊ท ์”จ 3
179
- ๊ณต๊ฐœํ•˜์ง€ 3
180
- ์ด์–ด์ง€๊ณ  3
181
- ๋ฌธ์ œ์˜€๋‹ค 3
182
- ์†ํ•ด๋ฐฐ์ƒ 3
183
- ๋งˆ์ด๋„ˆ์Šค 3
184
- ํ˜„๋Œ€๊ฑด์„ค 3
185
- ์ƒŒ๋””์—์ด๊ณ  3
186
- ์•ผ๋งˆ๋ชจํ†  3
187
- ์š”์‹œ๋…ธ๋ถ€ 3
188
- ์—๋ฅด๋‚œ๋ฐ์Šค 3
189
- ์„ ๋ฐœํˆฌ์ˆ˜ 3
190
- ๋ฌด์‹ค์ ์œผ๋กœ 3
191
- ์—ญ์ „์Šนํ–ˆ๋‹ค 3
192
- ํด๋ฆฌ๋ธ”๋žœ๋“œ 3
193
- ๊ฐ€๋””์–ธ์Šค 3
194
- ๋””ํŠธ๋กœ์ดํŠธ 3
195
- ์ด์Šน๋งŒ๋Œ€ํ†ต๋ น๊ธฐ๋…๊ด€ 3
196
- ์‹œ์ •๋ช…๋ น 3
197
- ์ง‘ํ–‰์ •์ง€ 3
198
- ๋ธ”๋ฃธ๋ฒ„๊ทธ 3
199
- ์‹œ์žฅ์—์„œ 3
200
- ์‹œ์žฅ์œผ๋กœ 3
201
- ์ˆ˜์ค€์ด๋‹ค 3
202
- ์ตœ๊ณ ์ธ๋ฏผํšŒ์˜ 3
203
- ์ฃผ์‹ํˆฌ์ž์ž๊ธˆ 3
204
- ์›์ ์œผ๋กœ 3
205
- ํ•œ๊ตญ์‹œ๋ฆฌ์ฆˆ 3
206
- ๋‚ ์นด๋กœ์šด 3
207
- ๋ชจ์Šต์œผ๋กœ 3
208
- ํ•œ๊ตญ๋ฌธํ•™๋ฒˆ์—ญ์› 3
209
- ๋ฌธํ•™ํ‰๋ก  3
210
- ์‹œ์ค‘์€ํ–‰๋“ค 3
211
- ์ธํ„ฐ๋„ท์€ํ–‰๋“ค 3
212
- ๊ทธ๋ ‡๋‹ค๊ณ  3
213
- ๋ผ์ดํ”„์น˜ํžˆ 3
214
- ์˜ค๋ฆฌ์˜ฌ์Šค 3
215
- ๊ณผํ•™์ž๋“ค 3
216
- ๊ธฐ์š”์‚ฌํ‚ค 3
217
- ๊ฒƒ์ด์—ˆ๋‹ค 3
218
- ์˜์–ด์‹์œผ๋กœ 3
219
- ๋งŒ๋ฆฌ์žฅ์„ฑ 3
220
- ์žฅํŽธ์†Œ์„ค 3
221
- ํ™•์ธ๋๋‹ค 3
222
- ์ค‘์•™์„œ๋ฆผ๋ณธ 3
223
- ๋Œ€ํ•™์ƒ๋“ค 3
224
- ์Šคํ‹ฐ๋กœํผ 3
225
- ๋ฐฉ์นจ์ด๋‹ค 2
226
- ๊ฐœ๋ฐœํ•˜๊ณ  2
227
- ๋ฐฐํ„ฐ๋ฆฌํŒฉ 2
228
- ์ง€์†์ ์œผ๋กœ 2
229
- ๋ฐœ์ „์‹œ์ผœ 2
230
- ์ฒ˜์Œ์œผ๋กœ 2
231
- ์กฐ์ •ํ–ˆ๋‹ค 2
232
- ๊ณต์‹œํ–ˆ๋‹ค 2
233
- ๋ชฉํ‘œ์ฃผ๊ฐ€ 2
234
- ์œ ์ง„ํˆฌ์ž์ฆ๊ถŒ 2
235
- ์ด์–ด์ง€๋ฉฐ 2
236
- ์ƒ๊ฐํ•˜์ง€ 2
237
- ์Œ์ฃผ์šด์ „ 2
238
- ํŒŒ์•…๋๋‹ค 2
239
- ์ฒด๋‚ฉ์œผ๋กœ 2
240
- ์ฐจ๋Ÿ‰์œผ๋กœ 2
241
- ์ถ”๊ฐ€๋๋‹ค 2
242
- ํŠน์ง•์ด๋‹ค 2
243
- ๊ฐ€์ „์ œํ’ˆ 2
244
- ์ œํ’ˆ์—์„œ 2
245
- ํดํฌ์Šค๋ฐ”๊ฒ 2
246
- ๋‚ด๋†“์œผ๋ฉฐ 2
247
- ์œ ์ง€ํ•˜๊ธฐ 2
248
- ์ผ๋ฐ˜์ ์œผ๋กœ 2
249
- ๋„์ž…๋œ๋‹ค 2
250
- ๋„๋กœ์—์„œ 2
251
- ์กฐ์‚ฌ์—์„œ 2
252
- ์ •๋„๋ผ๊ณ  2
253
- ๊ฑฑ์ •ํ•ด์•ผ 2
254
- ๋ชป์ง€์•Š๊ฒŒ 2
255
- ํ”Œ๋กœ๋ฆฌ๋‹ค 2
256
- ํž์ฆˆ๋ฒ„๋Ÿฌ 2
257
- ์œ ์„ธ์—์„œ 2
258
- ์ฃผ๋ฏผ์—๊ฒŒ 2
259
- ๋ˆ์งˆ๊ธฐ๊ฒŒ 2
260
- ์ฃผ๋„ํ•˜๊ณ  2
261
- ๋ธŒ๋ ŒํŠธ์œ  2
262
- ๊ณผ๋„ํ•˜๋‹ค 2
263
- ๋ถ„์„ํ–ˆ๋‹ค 2
264
- ์ถœ์‹œํ–ˆ๋‹ค 2
265
- ๋ฒ„์Šค์ค‘์•™์ฐจ๋กœ 2
266
- ์ง€๋‚˜์น˜๊ฒŒ 2
267
- ๋Œ€์ƒ์ด๋‹ค 2
268
- ์ด์ƒ์œผ๋กœ 2
269
- ๋–จ์–ด์กŒ๋‹ค 2
270
- ํฌ๋ฅดํˆฌ๊ฐˆ 2
271
- ํˆฌ์ž…ํ–ˆ๋‹ค 2
272
- ๋ฝ‘์•„๋‚ด๋ฉฐ 2
273
- ๋›ฐ์–ด์˜ฌ๋ž๋‹ค 2
274
- ์˜ฌ์ž‰๊ธ€๋žœ๋“œ 2
275
- ํŒฌ๋“ค์—๊ฒŒ 2
276
- ๋“œ๋Ÿฌ๋‚˜๊ธฐ 2
277
- ๋ฐฉํ–ฅ์œผ๋กœ 2
278
- ์„ ๋ณด์˜€๋‹ค 2
279
- ๊ตฌ๋งคํ•˜๊ณ  2
280
- ๋ถ„์„ํ•˜๊ณ  2
281
- ํ—Œ๋ฒ•์žฌํŒ๊ด€ 2
282
- ์ง„ํ–‰๋˜์ง€ 2
283
- ๊ณต๊ฐœ๋๋‹ค 2
284
- ํ™”์ดํŠธํ๋ธŒ 2
285
- ํ•˜๋ฃจํ•˜๋ฃจ 2
286
- ๋‰ด์š•์—์„œ 2
287
- ์ „๊ตญ์ ์œผ๋กœ 2
288
- ์‹œ์ž‘ํ•˜๋˜ 2
289
- ํ•„์š”ํ•˜๋‹ค 2
290
- ์˜ˆ๋…„๋ณด๋‹ค 2
291
- ๋งŽ์•˜๋Š”๋ฐ 2
292
- ์ž๋ณธ์‹œ์žฅ 2
293
- ํ์ง€๊นŒ์ง€ 2
294
- ์œ ์˜ˆํ•œ๋‹ค๊ณ  2
295
- ๊ฒŒ์ž„์—์„œ 2
296
- ํƒœ์–ด๋‚ฌ๋‹ค 2
297
- ๋“ฑ๊ทนํ–ˆ๋‹ค 2
298
- ์บ ๋ธŒ๋ฆฌ์ง€ 2
299
- ์ŠคํŠœ๋””์˜ค 2
300
- ๋‹น์ฒจ๋˜๊ธฐ 2
301
- ๋ฌธํ™”์ฒด์œก๊ด€๊ด‘๋ถ€ 2
302
- ์„ฑํฌ๋กฑ์œผ๋กœ 2
303
- ๋…ธ๋ฒจ์˜ํ•™์ƒ 2
304
- ๋…ธ๋ฒจ์œ„์›ํšŒ 2
305
- ๊ฒฐ์ •์ ์ธ 2
306
- ์—ฐ๊ตฌ์ž๋“ค 2
307
- ๋ฐํ˜€์กŒ๋‹ค 2
308
- ์กฐ์„ ์ผ๋ณด 2
309
- ์Šค๋งˆํŠธ์ธ์ปด 2
310
- ๊ณต๊ฐœ๋œ๋‹ค 2
311
- ์—…๋กœ๋“œ๋œ๋‹ค 2
312
- ์—ฐ๊ธˆ์ˆ˜๋ น 2
313
- ํ•„๋ช…์œผ๋กœ 2
314
- ๊ธฐ์ค€๊ธˆ๋ฆฌ 2
315
- ์ง„์ž…ํ–ˆ๋‹ค 2
316
- ์Šค๋ƒ…๋“œ๋ž˜๊ณค 2
317
- ์ฑ„ํƒํ–ˆ๋‹ค 2
318
- ํ™๋ช…๋ณดํ˜ธ 2
319
- ํ„ฐ๋œจ๋ ธ๋‹ค 2
320
- ์—๋ ˆ๋””๋น„์‹œ 2
321
- ์ด์–ด์ง€๋ฉด์„œ 2
322
- ๊ฒฝ์Ÿ์—์„œ 2
323
- ๋ณด์ด๋ฉด์„œ 2
324
- ๋Œ€ํ˜•๋งˆํŠธ 2
325
- ๋ถ์ ์˜€๋‹ค 2
326
- ์†Œ๋น„์ž๋“ค 2
327
- ๋ถ„์œ„๊ธฐ๋‹ค 2
328
- ๋ณธ๊ฒฉ์ ์œผ๋กœ 2
329
- ์ ‘์–ด๋“ค์ž 2
330
- ํ”„๋žœ์ฐจ์ด์ฆˆ 2
331
- ๊ธ‰์ฆํ–ˆ๋‹ค 2
332
- ์ฃผ๋ ฅํ•˜๊ณ  2
333
- ๊ณผ์„ธ๋‹น๊ตญ 2
334
- ์ ์šฉํ•˜๋ฉด 2
335
- ํ•ด์™ธ์—์„œ 2
336
- ๊ธฐํš์žฌ์ •๋ถ€ 2
337
- ์ด์šฉ์ž๋“ค 2
338
- ์„ฑ๋ƒฅ๊ฐ‘๋“ค 2
339
- ๋•Œ๋ฌธ์ด๋ผ 2
340
- ์ž์›์ˆœํ™˜ํ˜• 2
341
- ๋ถ„์•ผ์—์„œ 2
342
- ์ˆœ์œ„์—์„œ 2
343
- ์ง„ํ–‰ํ•œ๋‹ค 2
344
- ํ”Œ๋ผ์Šคํ‹ฑ 2
345
- ์ˆ˜์†Œ์ „๊ธฐ์ฐจ 2
346
- ๊ณต๋™์œผ๋กœ 2
347
- ํˆฌ์žํ•˜๊ณ  2
348
- ์ฆ์‹œ์—์„œ 2
349
- ์‚ฌ๋ชจํŽ€๋“œ 2
350
- ์ œ์ถœํ–ˆ๋‹ค 2
351
- ๊ฒฐ์ •ํ–ˆ๋‹ค 2
352
- ๋ณด๋„์ž๋ฃŒ 2
353
- ๋ณด๊ด€ํ•˜๋˜ 2
354
- ๊ทœ๋ช…ํ•˜์ž๋ฉฐ 2
355
- ํŠน๊ฒ€๋ฒ•์•ˆ 2
356
- ์ฆ์–ธ๊ฐ์ •๋ฒ• 2
357
- ์ƒ์„คํŠน๊ฒ€์œผ๋กœ 2
358
- ์ˆ˜์‚ฌํ•˜๊ฒŒ 2
359
- ์žฌ๋ฐœ์˜ํ•  2
360
- ํ™•๋ณดํ•˜๋ ค 2
361
- ์ฒ˜๋ฆฌํ•œ๋‹ค 2
362
- ๋ฌด๋ ฅํ™”ํ•˜ 2
363
- ์ด๋Œ์–ด๋‚ผ 2
364
- ์žฅ๋‹ดํ•˜๊ธฐ 2
365
- ์ƒํ™ฉ์—์„  2
366
- ๋…ธ๋ฆผ์ˆ˜๋กœ 2
367
- ์‚ฌ๋ฒ•๋ฆฌ์Šคํฌ 2
368
- ์žกํžˆ๋ฉด์„œ 2
369
- ์ œ๊ธฐ๋˜๊ณ  2
370
- ๋ฏผ์ฃผ๋‹น์œผ๋กœ์„  2
371
- ๊ฒฐ์ง‘ํ•˜๊ณ  2
372
- ๋Œ์–ด์˜ฌ๋ ค์•ผ 2
373
- ๋ฐ˜๋ณต๋˜๋ฉฐ 2
374
- ์ƒ์„คํŠน๊ฒ€๋ฒ• 2
375
- ํŒ๋‹จํ•˜๊ฑฐ๋‚˜ 2
376
- ๋ฐœ์˜ํ–ˆ๋‹ค 2
377
- ๋ฒ•์›ํ–‰์ •์ฒ˜ 2
378
- ๋Œ€ํ•œ๋ณ€ํ˜‘ํšŒ์žฅ 2
379
- ์ถ”์ฒœํ•˜๋„๋ก 2
380
- ๋ฐœ์˜ํ–ˆ๋˜ 2
381
- ๋ช…์‹œํ–ˆ๋˜ 2
382
- ๊ฑฐ๋ก ๋œ๋‹ค 2
383
- ๋งž๋‹ฅ๋œจ๋ฆด 2
384
- ์œ ์ง€ํ•˜๊ณ  2
385
- ์—ฐ๊ตฌ๊ฐœ๋ฐœ 2
386
- ์‚ฌ๊ฑด์—์„œ 2
387
- ์—ฐ๊ตฌ์œ„์› 2
388
- ์ „๋žต์œผ๋กœ 2
389
- ๊ฐ•์กฐํ–ˆ๋‹ค 2
390
- ๊ธฐ๋ฐ˜์œผ๋กœ 2
391
- ์ง€์›ํ•˜๊ณ  2
392
- ์น˜์—ดํ•ด์ง€๋ฉด์„œ 2
393
- ๊ธ‰์ฆํ•˜๊ณ  2
394
- ์—”์ง€๋‹ˆ์–ด 2
395
- ๊ตฌ์ถ•ํ•˜๊ณ  2
396
- ๊ธฐ๋กํ–ˆ๋Š”๋ฐ 2
397
- ํ•ด๋‹นํ•œ๋‹ค 2
398
- ํ„ฐ๋ฐ”์ธํ™€ 2
399
- ํ”„๋กœ์ ํŠธ 2
400
- ํ’ˆ์งˆ๊ด€๋ฆฌ๋‹จ 2
401
- ์›์น™์ ์œผ๋กœ 2
402
- ๋Œ€ํ–‰์—…์ฒด 2
403
- ๊ฑด์„ค์‚ฌ๋“ค 2
404
- ๋ชปํ•˜๋„๋ก 2
405
- ์ •์ƒํšŒ์˜ 2
406
- ์„ธ๊ณ„์—์„œ 2
407
- ๊ตญ์ œ์‚ฌํšŒ 2
408
- ์ด์„ ์—์„œ 2
409
- ๋ฆฌ์–ผ๋ฏธํ„ฐ 2
410
- ์„ ๊ฑฐ์—์„œ 2
411
- ์˜๊ด‘์œผ๋กœ 2
412
- ๋Œ์•„๋‹ค๋‹ˆ๋ฉฐ 2
413
- ์‚ฌ์ด์—์„  2
414
- ๋‹น์„ ๋˜๋“  2
415
- ์กธ์—…ํ–ˆ๋‹ค 2
416
- ์ง‘์ค‘ํ˜ธ์šฐ 2
417
- ํญ๋ฐœ์ ์œผ๋กœ 2
418
- ์Ÿ์•„๋‚ด๊ธฐ 2
419
- ๋Š˜์–ด๋‚˜์ง€ 2
420
- ๋‚ด๋‹ค๋ดค๋‹ค 2
421
- ๊ฐœํ˜์‹ ๋‹น 2
422
- ๊ณผ์‹œํ•˜๊ณ  2
423
- ๊ตญํšŒ์˜์› 2
424
- ๋ณด๊ถ์„ ๊ฑฐ 2
425
- ์ž…์žฅ์ด๋‹ค 2
426
- ์ •์น˜์ธ๋“ค 2
427
- ๋“ค์–ด๊ฐ”๋‹ค 2
428
- ๋งŒ๋“ค์–ด์กŒ๋‹ค 2
429
- ์•Š์•˜๋‹ค๊ณ  2
430
- ์‹œ๋„ํ•˜๊ฑฐ๋‚˜ 2
431
- ๋‘๋ถ€๊น€์น˜ 2
432
- ๊ตํ†ต์‚ฌ๊ณ  2
433
- ๊ณ ๊ฐ€์ฐจ๋„ 2
434
- ์ œํ’ˆ์œผ๋กœ 2
435
- ์ธํ…Œ๋ฆฌ์–ด 2
436
- ๊ตญ๋‚ด์—์„  2
437
- ์ค‘์š”ํ•˜๊ฒŒ 2
438
- ์—ฌ๋Ÿฌ๋ชจ๋กœ 2
439
- ์ „ํ™”๋ฒˆํ˜ธ 2
440
- ์••๋„์ ์ธ 2
441
- ๋ช…์”จ์—๊ฒŒ 2
442
- ์ฐจ๊ทผ์ฐจ๊ทผ 2
443
- ํ–ˆ์Šต๋‹ˆ๋‹ค 2
444
- ๋ฆฌ๋”๋ผ๋ฉด 2
445
- ๋ฒ•์›์—์„œ 2
446
- ์˜ˆ์ •์ด๋ผ๊ณ  2
447
- ํŒŒ๋ผ์‹œ์Šค 2
448
- ํ•˜๋ฝํ–ˆ๋‹ค 2
449
- ํ•œ๊ตญ๊ฑฐ๋ž˜์†Œ 2
450
- ์ค‘์†Œํ˜•์ฃผ 2
451
- ํ”„๋กœ๋ฐฐ๊ตฌ 2
452
- ๋ฒ—์–ด๋‚ฌ๋‹ค 2
453
- ๋“์ ์œผ๋กœ 2
454
- ๋ฐ”๋€Œ์—ˆ๋‹ค 2
455
- ์‹œ์ฆŒ๋ถ€ํ„ฐ 2
456
- ์ •๊ทœ๋ฆฌ๊ทธ 2
457
- ํŽ˜์–ดํ”Œ๋ ˆ 2
458
- ๋ฉ”์ด์ €๋ฆฌ๊ทธ 2
459
- ์•ˆ๋ฐฉ์—์„œ 2
460
- ์ œ์••ํ–ˆ๋‹ค 2
461
- ์ˆœ๊ฐ„์ด์—ˆ๋‹ค 2
462
- ๋’ค์ง‘์—ˆ๋‹ค 2
463
- ๋‹ค๋ฅด๋น—์Šˆ 2
464
- ํ•„๋ผ๋ธํ”ผ์•„ 2
465
- ๋ธŒ๋ฃจ์–ด์Šค 2
466
- ๋Œ€ํ•œ๋ฏผ๊ตญ 2
467
- ์—”์”จ์†Œํ”„ํŠธ 2
468
- ๊ธฐ๋กํ•˜๋ฉฐ 2
469
- ์ด์–ด๊ฐ”๋‹ค 2
470
- ์•Œ๊ณ ๋ฆฌ์ฆ˜ 2
471
- ์—…๊ณ„์—์„  2
472
- ์ง€๋‚œํ•ด์—” 2
473
- ๋…ผ์˜ํ•˜๊ธฐ 2
474
- ์˜ˆ๊ณ ํ–ˆ๋‹ค 2
475
- ๊ตญ๋ฌด์œ„์›์žฅ 2
476
- ๊ณต๊ฐœ์ ์œผ๋กœ 2
477
- ์ฆ๊ถŒํˆฌ์ž์ž๊ธˆ 2
478
- ๋‚จํŽธ์ด๋‚˜ 2
479
- ํœด๋Œ€์ „ํ™” 2
480
- ํ”„๋กœ์•ผ๊ตฌ 2
481
- ์ˆ˜์›์—์„œ 2
482
- ์ ์‹œํƒ€๋กœ 2
483
- ์—˜๋ฆฌ์—์ด์ € 2
484
- ์•„์‹œ์•„๋‚˜ํ•ญ๊ณต 2
485
- ๊ฐ€๋ฆฌํ‚จ๋‹ค 2
486
- ์ง‘์ค‘ํ–ˆ๋‹ค 2
487
- ๋Œ€ํ•œ๋ฐฐ๋“œ๋ฏผํ„ดํ˜‘ํšŒ 2
488
- ์ฒœ์œ„ํŽ˜์ด 2
489
- ์ž‘ํ’ˆ์ด๋ผ 2
490
- ํ‰๊ฐ€ํ–ˆ๋‹ค 2
491
- ์ˆ˜์ค€์œผ๋กœ 2
492
- ๋Œ€์ค‘๋ฌธํ™” 2
493
- ์‹ ์šฉ๋Œ€์ถœ 2
494
- ์ฃผํƒ๋‹ด๋ณด๋Œ€์ถœ 2
495
- ์ฒœ์—ฐ๊ฐ€์Šค 2
496
- ๋ถ„๋ฐ์Šค๋ฆฌ๊ฐ€ 2
497
- ๋ฏผ์ฃผ์ฃผ์˜ 2
498
- ์‹œ์ž‘๋œ๋‹ค 2
499
- ๋ฏธ์„ธ์ „๋ฅ˜ 2
500
- ์ฑ”ํ”ผ์–ธ์‹ญ 2
501
- ์›”๋“œ์‹œ๋ฆฌ์ฆˆ 2
502
- ์นด๋ฅผ๋กœ์Šค 2
503
- ์—์Šคํ…Œ๋ฒ ์ฆˆ 2
504
- ๋ ˆ๋“œ์‚ญ์Šค 2
505
- ํ„ฐ๋œจ๋ฆฌ๋ฉฐ 2
506
- ๊ด€์ ์—์„œ 2
507
- ๋Œ์•„์„ฐ๋‹ค 2
508
- ๋“œ๋ผํ˜๋ผ 2
509
- ์ธํ„ฐ๋‚ด์…”๋„ 2
510
- ๋งจ๋ถ€์ปค์ƒ 2
511
- ๋ฉ”๋””์น˜์ƒ 2
512
- ๋ง๋ ˆ์ด์‹œ์•„ 2
513
- ๋ธŒ๋ฃจ๋‚˜์ด 2
514
- ์„ธ๊ณ„์ ์ธ 2
515
- ํˆฌ์žํ•ด์•ผ 2
516
- ์ฆ๊ฐ€ํ•œ๋‹ค 2
517
- ๋†’์•„์ง€๊ณ  2
518
- ์นœ์•„๋ฒ„์ง€ 2
519
- ๊ณต๋ถ€ํ•ด์•ผ 2
520
- ํ•˜์ดํŠธ์ง„๋กœ 2
521
- ๋ฟŒ๋ฆฌ์ณค๋‹ค 2
522
- ์šฐ์Šนํ•˜๊ณ  2
523
- ๋“œ๋ผ์ด๋ธŒ์ƒท 2
524
- ์ง€ํ‚ค๋А๋ƒ 2
525
- ํ•ด๊ฒฐํ–ˆ๋‹ค 2
526
- ์žก์•„๋‹น๊ฒจ 2
527
- ํ‰๊ท ์ž์ฑ…์  2
528
- ๊ฐ€์ ธ์™”๋‹ค 2
529
- ๊ฐ€๋ฅด์‹œ์•„ 2
530
- ์ƒŒํ”„๋ž€์‹œ์Šค์ฝ” 2
531
- ์ •๊ทœ์‹œ์ฆŒ 2
532
- ์• ํ‹€๋žœํƒ€ 2
533
- ๋…์ผ์–ด์ •์„œ๋ฒ•์œ„์›ํšŒ 2
534
- ํ”๋“ค๋„๋ก 2
535
- ์‹œ๋‹ฌ๋ ธ๋‹ค 2
536
- ๋‹ˆ์ฝ”๋ฐ”๋ฅด 2
537
- ์œ ๋Šฅํ•˜๊ฒŒ 2
538
- ์„ค์น˜ํ–ˆ๋‹ค 2
539
- ๋‹น์„ ๋˜๋ฉฐ 2
540
- ์ด์ƒ๋ฌธํ•™์ƒ 2
541
- ๊ด‘์ฃผ๋ฏผ์ฃผํ™”์šด๋™ 2
542
- ๋ฌธ์žฅ์œผ๋กœ 2
543
- ํŠธ๋ผ์šฐ๋งˆ 2
544
- ๋น„์„œ์‹ค์žฅ 2
545
- ๋ถ€์‚ฐ์—์„œ 2
546
- ํ–‰๋™๊ฐ•๋ น 2
547
- ์„ฑ๊ณตํ–ˆ๋‹ค 2
548
- ์‹ค๋ง์Šค๋Ÿฌ์šด 2
549
- ๋น„๊ตํ•˜๋ฉด 2
550
- ์œค์ „์‹์œผ๋กœ 2
551
- ๊ตญ๊ฐ€์œ ์‚ฐ์œผ๋กœ 2
552
- ๋ถํ•œ์—ฐ๊ตฌ์†Œ 2
553
- ํƒˆ๋ถ๋ฏผ๋“ค 2
554
- ์ด์ฐธ๋ชจ๋ถ€ 2
555
- ํ”Œ๋žซํผ๋“ค 2
556
- ์ฟ ํŒก์ด์ธ  2
557
- ์•„๋ฏธ๋…ธ์‚ฐ๋“ค 2
558
- ์•„๋ฏธ๋…ธ์‚ฐ 2
559
- ๊ฐœ๋ฐœํ–ˆ๋‹ค 2
560
- ์ด์‚ฐํ™”ํƒ„์†Œ 2
561
- ํ˜์‹ ๊ธฐ์—… 2
562
- ํ‰๊ท ๊ธฐ์˜จ 2
563
- ๋–จ์–ด์ง€์ง€ 2
564
- ์ตœ๊ณ ๊ธฐ์˜จ 2
565
- ์ง‘ํ–‰์œ„์›์žฅ 2
566
- ํ™•๋Œ€ํ•˜๊ณ  1
567
- ์•ˆ์‹ฌ์ ๊ฒ€ 1
568
- ๋ฌด์ƒ๋ณด์ฆ๊ธฐ๊ฐ„ 1
569
- ๋ฒ•์ธ์ฐจ๋กœ 1
570
- ์ „๊ธฐ์ฐจ์—” 1
571
- ํ†ต๋ณด๋˜์ง€ 1
572
- ๋“ฑ๋กํ•˜๋ฉด 1
573
- ์ง„์ž…ํ•˜๊ธฐ 1
574
- ๋ฐœ์ƒํ•˜๋ฉด 1
575
- ํˆฌ์ž…๋œ๋‹ค 1
576
- ์†Œ๋ฐฉ์ฒญ์—์„  1
577
- ์‹œ์ž‘ํ•˜๊ณ  1
578
- ํŠน์ˆ˜๊ตฌ์กฐ๋Œ€ 1
579
- ๊ธฐ์ฆํ•œ๋‹ค 1
580
- ์ˆ˜์••์œผ๋กœ 1
581
- ์•ˆ์‹ฌํ•˜๊ณ  1
582
- ๋Œ€ํ‘œ์ฃผ์ธ 1
583
- ์‹ ํ•œํˆฌ์ž์ฆ๊ถŒ 1
584
- ํ•ด๋‹น๋˜์ง€ 1
585
- ์†Œ์™ธ๋˜๊ณ  1
586
- ์ˆœ๋งค๋„ํ–ˆ์ง€ 1
587
- ์ˆœ๋งค์ˆ˜ํ–ˆ๋‹ค 1
588
- ๊ณ ๋Œ€์—ญํญ 1
589
- ๊ทธ๋ ˆ์ด๋“œ 1
590
- ์ฆ๊ถŒ์‚ฌ๋“ค 1
591
- ์ง‘๊ณ„๋๋‹ค๊ณ  1
592
- ํ•˜ํšŒํ–ˆ๋‹ค 1
593
- ํ˜„๋Œ€์ฐจ์ฆ๊ถŒ 1
594
- ๋ฐœํ‘œ์—์„œ 1
595
- ์ง€์ผœ์ง€์ง€ 1
596
- ๋น„๋ฉ”๋ชจ๋ฆฌ 1
597
- ๋น„์šฉ์œผ๋กœ 1
598
- ์—ฐ๋ง๊นŒ์ง€ 1
599
- ๊ทผ์ ‘ํ•˜๋ฉฐ 1
600
- ํ•˜ํšŒํ•˜๊ณ  1
601
- ๋™๊ธฐ๋ณด๋‹ค 1
602
- ์ƒ์Šนํ•˜๋„๋ก 1
603
- ์ดˆ์ž…์ด๋ผ 1
604
- ๋งž์ถ”๋ฉด์„œ 1
605
- ๏ฟฝ๏ฟฝ๋นกํ•ด์งˆ 1
606
- ์˜ˆ์ƒํ–ˆ์ง€ 1
607
- ์Šค๋งˆํŠธํฐ์šฉ 1
608
- ๊ธฐ๋Œ€ํ•˜๊ธฐ 1
609
- ์–ด๋ ค์›Œ์กŒ๋‹ค๊ณ  1
610
- ์ถ”๊ฒฉ์ด๋‹ค 1
611
- ์„ฑ๊ณตํ•˜๋“  1
612
- ์‹คํŒจํ•˜๋“  1
613
- ์—”๋น„๋””์•„ 1
614
- ํ†ต๊ณผํ•˜๋ฉด 1
615
- ์œ ์˜๋ฏธํ•œ 1
616
- ๋–จ์–ด์ง€๊ฒŒ 1
617
- ์ƒํƒœ์˜€๋˜ 1
618
- ๋ชจ๋ธ์ด๋‹ค 1
619
- ๊ฐœ์ธ์ ์œผ๋กœ 1
620
- ์‚ฌ์šฉํ•˜๊ธฐ 1
621
- ์ž๋™์ฐจ๋“ฑ๋ก์›๋ถ€ 1
622
- ์†Œ์œ ์˜€๋˜ 1
623
- ์œ„๋ฐ˜์œผ๋กœ 1
624
- ์ œ์ฃผ์—์„œ 1
625
- ์–‘๋„ํ–ˆ๋‹ค 1
626
- ๋ช…์˜์˜€๋‹ค 1
627
- ์ด์ „๋๊ณ  1
628
- ๋ฌธ์”จ์—์„œ 1
629
- ๋Œ€ํ†ต๋ น์œผ๋กœ 1
630
- ์ด์ „๋๋‹ค 1
631
- ์ด๋ฉด๋„๋กœ 1
632
- ๋Œ์•„์™”๋‹ค 1
633
- ํ•ด๋ฐ€ํ†คํ˜ธํ…” 1
634
- ์šด์ „ํ•˜๋˜ 1
635
- ๋ณ€๊ฒฝํ•˜๋‹ค 1
636
- ๋’ค๋”ฐ๋ผ์˜ค๋˜ 1
637
- ํ”ผํ•ด์ž์ธ 1
638
- ํ˜ˆ์ค‘์•Œ์ฝ”์˜ฌ๋†๋„ 1
639
- ์ˆ˜์ค€์ด์—ˆ๋‹ค 1
640
- ์นด๋ฉ”๋ผ์šฉ 1
641
- ์“ธ์–ด์ฃผ๋ฉด 1
642
- ์ถ”๊ฐ€ํ–ˆ๋Š”๋ฐ 1
643
- ์‚ฌ๋ผ์กŒ๋˜ 1
644
- ๋Œ์•„์˜ค๊ณ  1
645
- ๋˜๋Œ์•„๊ฐ€๊ณ  1
646
- ์• ํ”Œ๋งˆ์ € 1
647
- ๊ฐ€์ ธ์˜ค๊ณ  1
648
- ๊ธฐ์‚ฌ์—์„œ 1
649
- ์“ธ์–ด๋„˜๊ธฐ๋˜ 1
650
- ๋””์Šคํ”Œ๋ ˆ์ด 1
651
- ์žฅ์ฐฉํ–ˆ๋˜ 1
652
- ๋“ฑ์žฅํ•˜๊ณ  1
653
- ์ฃผ๋ชฉ๋ฐ›๊ณ  1
654
- ๋„์ž…ํ•˜๊ฒ ๋‹ค๊ณ  1
655
- ์ฐจ์ข…์—์„œ 1
656
- ํšŒ๊ท€ํ–ˆ๋‹ค 1
657
- ์˜ฌ๋ฆฌ๊ฑฐ๋‚˜ 1
658
- ํ—ค๋ฆฌํ‹ฐ์ง€ 1
659
- ๋ธŒ๋ ˆ์ดํฌ 1
660
- ์ถ”๊ฐ€๋˜๊ณ  1
661
- ์„ ๋ณด์ด๋ฉฐ 1
662
- ์ถ”๊ฐ€ํ–ˆ๋‹ค 1
663
- ์ €์žฅํ•ด๋‘˜ 1
664
- ์Šคํ†ฑ์›Œ์น˜ 1
665
- ๋‹จ์ถ•ํ‚ค๋กœ 1
666
- ์žฅ์ ์ด๋‹ค 1
667
- ๋„˜๊ธฐ๊ฒŒ๋” 1
668
- ๊ฐ์ง€๋˜๊ณ  1
669
- ์ฃผ์‹œํ•ด์•ผ 1
670
- ํ™”๋ฉด์—์„œ 1
671
- ํ”„๋กœ๊ทธ๋žจ์ธ 1
672
- ๋ฐ›์œผ๋ ค๋ฉด 1
673
- ์ฑ…์ž„์ž์ธ 1
674
- ์—์ด๋ฒ„๋ฆฌ 1
675
- ๋”ํƒ€์ž„์Šค์™€ 1
676
- ์ œ์กฐ์—…์ฒด 1
677
- ํ„ฐ์น˜์Šคํฌ๋ฆฐ์œผ๋กœ 1
678
- ์‚ฐ๋งŒ์œผ๋กœ 1
679
- ๋งˆ์ฐฌ๊ฐ€์ง€๋‹ค 1
680
- ๊ฐ€์Šค๋ ˆ์ธ์ง€ 1
681
- ๋™์ผํ•˜๊ฒŒ 1
682
- ํ˜๋Ÿฌ๋„˜์น˜๋ฉด 1
683
- ์ž‘๋™ํ•˜์ง€ 1
684
- ํŠธ๋ Œ๋“œ์™€ 1
685
- ๋งฅ๋ฝ์ด๋‹ค 1
686
- ํƒœ๋ธ”๋ฆฟ์šฉ 1
687
- ์ง„๋™์œผ๋กœ 1
688
- ํƒ‘์žฌํ•˜๊ณ  1
689
- ๊ฐ€๋ฅด์น˜๋ฉฐ 1
690
- ์ฒœ์ˆ˜๋‹ต์ด์–ด์„œ 1
691
- ์ƒ์†Œํ–ˆ๊ธฐ 1
692
- ์„ค๋ช…ํ•˜์ž 1
693
- ๊ทธ์ œ์„œ์•ผ 1
694
- ๋„๋•์˜€๋‹ค 1
695
- ์ง€์ ๋๋‹ค 1
696
- ๋ชปํ•œ๋‹ค๊ณ  1
697
- ์ฝ”๋ฏธ๋””๊ฐ™ 1
698
- ๋ฒŒ์–ด์ง„๋‹ค 1
699
- ์„ค๋ฌธ์—์„œ 1
700
- ๋Œ€๋‹ตํ–ˆ๋‹ค 1
701
- ๊ฑฑ์ •ํ•˜๊ธฐ 1
702
- ์งˆ๋ฌธ์ด๋‚˜ 1
703
- ์ดํ•ดํ• ๊นŒ 1
704
- ์ง€๊ฒฝ์ด๋ผ๊ณ  1
705
- ํŒŒ์•…ํ•œ๋‹ค 1
706
- ์•Œ์•„๋“ฃ๊ณ  1
707
- ๋„๋•์ธ๋‹ค 1
708
- ํ•œ์ž์–ด์ด๊ธฐ 1
709
- ์ดํ•ด๋ ฅ์ธ 1
710
- ๋ฉ€๋ฆฌํ•˜๊ณ  1
711
- ์ฝ์œผ๋ผ๊ณ  1
712
- ์ถ”์ƒ์ ์ธ 1
713
- ๋ชจ๋ฅด๊ณ ์„  1
714
- ์–ธ์–ด์ฒ ํ•™์ž์ธ 1
715
- ๋„“์–ด์ง„๋‹ค 1
716
- ์„ธ์ƒ์œผ๋กœ 1
717
- ์‚ฌ๋งํ–ˆ๋‹ค๊ณ  1
718
- ์ €๊ธฐ์••์œผ๋กœ 1
719
- ์•ฝํ™”๋์ง€ 1
720
- ๋ฐ€ํ„ด์œผ๋กœ 1
721
- ๋Š˜์—ˆ๋‹ค๊ณ  1
722
- ๋‚˜๋ญ‡๊ฐ€์ง€ 1
723
- ์ถ”์ •๋œ๋‹ค 1
724
- ๊ตฌ์ถœํ–ˆ๋‹ค 1
725
- ํƒฌํŒŒ์—์„œ 1
726
- ๊ตฌ์กฐ๋‹จ์ฒด 1
727
- ๊ตฌ์กฐํ–ˆ๋‹ค 1
728
- ๊ตฌ์กฐ๋Œ€์› 1
729
- ๋–จ์–ด์กŒ๊ณ  1
730
- ์•ฝํ™”๋๋‹ค 1
731
- ์ง€์—ญ์—์„œ 1
732
- ๋Œ€์„œ์–‘์œผ๋กœ 1
733
- ๋น ์ ธ๋‚˜๊ฐ”๋‹ค 1
734
- ๋‹ดํ™”์—์„œ 1
735
- ๋ถˆ์•ˆํ•˜๊ฒŒ 1
736
- ๋ฌด๋ชจํ•˜๊ณ  1
737
- ๋ฌด์ฑ…์ž„ํ•˜๋ฉฐ 1
738
- ๋น„ํŒํ–ˆ๋‹ค 1
739
- ํผ๋œจ๋ฆฌ๊ณ  1
740
- ๋น„๋‚œํ•˜๋ฉด์„œ 1
741
- ๋‰ด์Šค๋ ˆํ„ฐ 1
742
- ๊ตฌ๋…ํ•˜๊ธฐ 1
743
- ๋‚˜์˜ค๋ฉด์„œ 1
744
- ๊ธ‰๋“ฑํ–ˆ๋‹ค 1
745
- ์„œ๋ถ€ํ…์‚ฌ์Šค์‚ฐ 1
746
- ๊ฑฐ๋ž˜์ผ๋ณด๋‹ค 1
747
- ์ „๋‚ ๋ณด๋‹ค 1
748
- ๋ณด๋ณต์œผ๋กœ 1
749
- ๋‚˜์„œ๋ฉด์„œ 1
750
- ๋ถ€์ถ”๊ฒผ๋‹ค 1
751
- ์ง€์˜ค๋ฐ”๋‹ˆ 1
752
- ์Šคํƒ€์šฐ๋…ธ๋ณด 1
753
- ์• ๋„๋ฆฌ์ŠคํŠธ 1
754
- ์ •๋ฆฌํ•˜๋ฉด์„œ 1
755
- ์‚ฐ์œ ๊ตญ์ธ 1
756
- ์žฌ๊ทœ์–ด๋žœ๋“œ๋กœ๋ฒ„์ฝ”๋ฆฌ์•„ 1
757
- ์ธ์ œ๋‹ˆ์›€ 1
758
- ํ‘œ๋ฐฉํ•œ๋‹ค 1
759
- ๋ฆฌํด๋ผ์ธ 1
760
- ์—‡๋น„์Šทํ•˜์ง€ 1
761
- ํ™œ์šฉํ•œ๋‹ค 1
762
- ์ˆ˜๋‚ฉ๊ณต๊ฐ„ 1
763
- ์˜คํ”„๋กœ๋“œ 1
764
- ๊ฐ•์ ์ด๋‹ค 1
765
- ์„œ์ŠคํŽœ์…˜ 1
766
- ํŽธ์•ˆํ•˜๊ฒŒ 1
767
- ๋…ธ๋ฉด์—์„œ 1
768
- ์ˆ˜์‹ฌ๊นŒ์ง€ 1
769
- ์•ˆ์ „ํ•˜๊ฒŒ 1
770
- ํŒ๋งค๋œ๋‹ค 1
771
- ๊ฐœํŽธํ•œ๋‹ค 1
772
- ์ค€๊ณต์˜์ œ 1
773
- ๋„์ž…ํ•˜๋ฉด์„œ 1
774
- ์žฌ์„ค๊ณ„ํ•ด 1
775
- ์„œ์šธ์‹œ๋ฒ„์Šค์šด์†ก์‚ฌ์—…์กฐํ•ฉ 1
776
- ๋…ธ์„ ์ฒด๊ณ„ 1
777
- ๋ฐœ์ฃผํ–ˆ๋‹ค 1
778
- ์‹œํ–‰ํ•œ๋‹ค 1
779
- ์‹ ์„ค๋๋Š”๋ฐ 1
780
- ๊ทธ๋Œ€๋กœ๋ผ 1
781
- ์œ„๋ก€์‹ ์‚ฌ์„  1
782
- ๊ฐœํ†ตํ•˜๊ณ  1
783
- ์—ฐ์žฅ๋œ๋‹ค 1
784
- ์ด๊ณณ์ €๊ณณ 1
785
- ํ†ต๊ณผํ•˜๋‹ค 1
786
- ๊ตฌ๋ถˆ๊ตฌ๋ถˆํ•œ 1
787
- ๋…ธ์„ ์œผ๋กœ 1
788
- ๊ณ ๋•๊ฐ•์ผ์ง€๊ตฌ 1
789
- ๊ทธ์—ˆ๋Š”๋ฐ 1
790
- ํŽธ๋ฆฌํ•ด์งˆ 1
791
- ์˜ˆ์ƒํ•œ๋‹ค 1
792
- ์„œ์šธ์‹œ๋‚ด 1
793
- ๋ฐ”๋€Œ์—ˆ๋Š”๋ฐ 1
794
- ๋ฐ˜์˜ํ•˜์ง€ 1
795
- ๋ฐฑ์ง€์—์„œ 1
796
- ํšจ์œจ์ ์ด๋ผ๊ณ  1
797
- ๊ฐ•๋“ฑ๋๋‹ค 1
798
- ์˜์ž…ํ•œ๋‹ค 1
799
- ์ฝ”๋ฆฐ์น˜์•ˆ์Šค 1
800
- ์ถœ์‹ ์œผ๋กœ 1
801
- ๋ฆฌ๊ทธ์—์„œ 1
802
- ๋Œ์–ด์˜ฌ๋ ธ๋‹ค 1
803
- ๊ตฌ๋‹จ์ด๋ผ 1
804
- ๋„‰๋„‰ํ•˜์ง€ 1
805
- ์ŠคํŠธ๋ผ์ด์ปค 1
806
- ๋“์ ํฌ๋กœ 1
807
- ์ฝ”๋ฆฌ์•„์ปต 1
808
- ์˜ฌ๋ ค๋†“์•˜๋‹ค 1
809
- ๋Œ€๊ตฌ์—์„œ 1
810
- ๋ ˆ์ „๋“œ๋‹ค 1
811
- ์–ด๋ ต๊ฒ ์ง€ 1
812
- ์„ธ์ง•์•ผ์—๊ฒ 1
813
- ํŠ€๋ฅดํ‚ค์˜ˆ 1
814
- ์ „์ „ํ•˜๋‹ค 1
815
- ํ™œ์•ฝํ•˜๊ณ  1
816
- ์—ฌ์ „ํ•˜๋‹ค 1
817
- ๋ฌด๋Œ€์˜€๋‹ค 1
818
- ํ—ˆ์šฉํ•˜๋ฉด์„œ 1
819
- ์ ˆ์ฒด์ ˆ๋ช… 1
820
- ํ™œ์šฉํ•ด๋ณด๊ฒ ๋‹ค 1
821
- ํŒŒ๊ณ ๋“ค์–ด 1
822
- ์ •ํ™•ํ•˜๊ณ  1
823
- ์žฌ์—ญ์ „์Šน 1
824
- ๋งŒ๋ฝํ–ˆ๋‹ค 1
825
- ์ž๋™์œผ๋กœ 1
826
- ๋–จ์–ด์ง€๊ณ  1
827
- ์–ด๋–ป๊ฒŒ๋“  1
828
- ๋งค์ง„์‹œํ‚ค๋ฉฐ 1
829
- ์šฐ๋ฆฌ์—๊ฒŒ 1
830
- ํ–‰๋ณตํ•˜๊ฒŒ 1
831
- ๊ฒฐ์Šน์ „์ด๋ž€ 1
832
- ๋Œ€ํšŒ๋ถ€ํ„ฐ 1
833
- ์‹œํ–‰ํ•œ๋‹ค๊ณ  1
834
- ์นด๋ฉ”๋ผ๋กœ 1
835
- ์ž…์ฒด์ ์œผ๋กœ 1
836
- ์žฌ๊ตฌ์„ฑํ•˜ 1
837
- ์ค‘๋ฐ˜๋ถ€ํ„ฐ 1
838
- ์ต์ˆ™ํ•˜๋‹ค 1
839
- ๊ทน๋Œ€ํ™”ํ•  1
840
- ๋Œ€ํšŒ๊นŒ์ง€ 1
841
- ๊ณ ์ˆ˜ํ–ˆ๋‹ค 1
842
- ์ œํ•œ์ ์œผ๋กœ 1
843
- ํ™œ์šฉํ–ˆ๋‹ค 1
844
- ์˜ˆ์„ ๋ถ€ํ„ฐ 1
845
- ๊ฒฐ์Šน๊นŒ์ง€ 1
846
- ๋Œ€์‹ ํ•œ๋‹ค 1
847
- ์‹ฌํŒํ˜‘ํšŒ 1
848
- ์–ธ์ œ๊นŒ์ง€ 1
849
- ์‚ฌ์šฉ๋๋‹ค 1
850
- ์˜คํ”ˆ์—์„  1
851
- ์šด์˜ํ–ˆ๋‹ค 1
852
- ์ฝ”ํŠธ์—์„œ 1
853
- ์„ค์น˜๋˜์–ด 1
854
- ์ฐธ๊ณ ์šฉ์œผ๋กœ 1
855
- ์ง„ํ™”ํ•˜๊ณ  1
856
- ๋ฆฌ๊ทธ์—์„  1
857
- ์‹œ์Šคํ…œ์œผ๋กœ 1
858
- ์˜คํ”„์‚ฌ์ด๋“œ 1
859
- ์žก์•„๋‚ด๊ณ  1
860
- ๋ฆฌํ”Œ๋ ˆ์ด 1
861
- ํ™ˆ๋Ÿฐ์ด๋‚˜ 1
862
- ํŒ๋…์—์„œ 1
863
- ์˜ฌํ•ด๋ถ€ํ„ฐ 1
864
- ์„œ๋น„์Šค์„ผํ„ฐ 1
865
- ๊ฐœ์ตœํ•˜๊ณ  1
866
- ๋งŒ์กฑํ–ˆ๋‹ค๊ณ  1
867
- ์‘๋‹ตํ–ˆ๊ณ  1
868
- ์‘๋‹ตํ–ˆ๋‹ค 1
869
- ์ฐจ๋Ÿ‰์—์„œ 1
870
- ๋งŒ์กฑ์Šค๋Ÿฌ์šด 1
871
- ๊ธฐ๋Šฅ์œผ๋กœ 1
872
- ์กฐ์„ฑํ•˜๊ธฐ 1
873
- ํƒ‘์žฌํ–ˆ๋‹ค 1
874
- ๊ฐ์ง€ํ•˜๊ณ  1
875
- ๋›ฐ์–ด๋‚˜๊ณ  1
876
- ์šฐ์ˆ˜ํ•˜๋‹ค 1
877
- ์ˆ˜์ค€์ด๊ณ  1
878
- ๊ตฌ๊ฐ„์—์„œ 1
879
- ๊ณ ์†๋„๋กœ๋‚˜ 1
880
- ์ฃผ๋ง๋งˆ๋‹ค 1
881
- ๋ฐฉ์†กํ†ต์‹ ์œ„์›์žฅ 1
882
- ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉด์„œ 1
883
- ํ—Œ์žฌ์†Œ์žฅ 1
884
- ์„ ์ถœํ•ด์•ผ 1
885
- ๋Œ€๋ฆฝ์œผ๋กœ 1
886
- ์ถœ์„ํ•ด์•ผ 1
887
- ์—ด๊ฒ ๋‹ค๊ณ  1
888
- ๋ฐํžˆ๋ฉด์„œ 1
889
- ํ—Œ๋ฒ•์žฌํŒ์†Œ๋ฒ• 1
890
- ์•Œ์•„๋ด์•ผ 1
891
- ์„ ์ถœํ•˜์ง€ 1
892
- ๊ฒ€ํ† ํ•ด๋ณด๋ผ 1
893
- ์ถœ์‹ ์ด๋‹ค 1
894
- ์ž‘์—…ํ–ˆ๋˜ 1
895
- ๋งˆ๋ฅด์„ธ์œ  1
896
- ํฉ๋ฟŒ๋ฆฌ๊ณ  1
897
- ์ž‘ํ’ˆ์ด๋‹ค 1
898
- ์‹œ๋ฆฌ์ฆˆ๋กœ 1
899
- ์ƒ๊ฐ์œผ๋กœ 1
900
- ์ „์‹œ๋œ๋‹ค 1
901
- ๋ฐœํ–‰๋œ๋‹ค 1
902
- ์˜ค๋ธŒ๋ฆฌ์ŠคํŠธ 1
903
- ์„œํŽœํƒ€์ธ 1
904
- ์˜ˆ์ˆ ๊ฐ๋… 1
905
- ์ง„ํ–‰ํ–ˆ๋˜ 1
906
- ์ฐจ๋ณ„์ด๋ผ๊ณ  1
907
- ์ œํ•œํ•˜์ง€ 1
908
- ๊ถŒ๊ณ ํ–ˆ๋‹ค 1
909
- ์•„๋‚ด์—๊ฒŒ 1
910
- ์‚ฌ์ฃผ๋ ค๊ณ  1
911
- ๋‚จ์„ฑ์—๊ฒŒ 1
912
- ๊ฑฐ๋ถ€ํ–ˆ๋‹ค๊ณ  1
913
- ์ œ๊ธฐํ–ˆ๊ณ  1
914
- ๊ฐ€์ž…์ž๊ฒฉ 1
915
- ๋ช…์‹œํ•˜๊ณ  1
916
- ํƒ“์ด๋ผ๊ณ  1
917
- ์ œํ•œํ•œ๋‹ค 1
918
- ์—ฌ์„ฑ์šฉ์œผ๋กœ 1
919
- ๋ถ€์กฑํ•˜๋‹ค๊ณ  1
920
- ์„ค๋ฆฝ๋๋˜ 1
921
- ๋‹ค์ˆ˜์˜€์Œ 1
922
- ๊ณ ๋ คํ•˜๋”๋ผ 1
923
- ์ถ”์ง„๋˜๋ฉด 1
924
- ๊ฐ€์ •์ฃผ๋ถ€ 1
925
- ๋”ฐ๊ฐ€์›€์œผ๋กœ 1
926
- ์•Œ๋ ˆ๋ฅด๊ธฐ์„ฑ 1
927
- ์ฃผ๋ฒ”์œผ๋กœ 1
928
- ๋ถ„ํฌํ•˜๋ฉฐ 1
929
- ์ฃผ๋ณ€์ด๋‚˜ 1
930
- ์‚ฐ์ฑ…ํ•˜๋ฉด 1
931
- ์‹ฌํ•ด์ง„๋‹ค 1
932
- ์‹œ์ฆŒ๋งˆ๋‹ค 1
933
- ํ™˜์ž์ด๊ธฐ 1
934
- ๊ธด์žฅํ•˜๋ฉฐ 1
935
- ์‹œ๋‹ฌ๋ฆฌ๊ณ  1
936
- ํ•˜์ˆœ๊นŒ์ง€ 1
937
- ์‚ผ์„ฑ์„œ์šธ๋ณ‘์› 1
938
- ์•Œ๋ ˆ๋ฅด๊ธฐ๋‚ด 1
939
- ๊ฑด์กฐํ•ด์ง€๊ณ  1
940
- ์„ ์„ ํ•ด์ง€๊ธฐ 1
941
- ๊ฑด๊ฐ•๋ณดํ—˜์‹ฌ์‚ฌํ‰๊ฐ€์› 1
942
- ์ถ”์‚ฐ๋œ๋‹ค 1
943
- ์•„์—ด๋Œ€์„ฑ 1
944
- ๋ณ€ํ•˜๋ฉด์„œ 1
945
- ์„ ์„ ํ•˜๊ณ  1
946
- ๊ณผ๋ฏผ๋ฐ˜์‘์œผ๋กœ 1
947
- ์žฌ์ฑ„๊ธฐ๋‚˜ 1
948
- ๊ฑด์กฐํ•ด์ง€๊ธฐ 1
949
- ์ง€์†๋˜๋ฉด์„œ 1
950
- ์š”์ธ์œผ๋กœ 1
951
- ์ž‘์šฉํ•˜๋ฉด์„œ 1
952
- ๋งŽ์•„์กŒ๋‹ค 1
953
- ์˜ˆ์ƒ๋˜๋‹ˆ 1
954
- ์งˆ๋ณ‘๊ด€๋ฆฌ์ฒญ 1
955
- ์งˆ๋ณ‘์ด๋‚˜ 1
956
- ์ฏ”์ฏ”๊ฐ€๋ฌด์‹œ๋ณ‘ 1
957
- ๊ธธ์–ด์ง€๋ฏ€๋กœ 1
958
- ๊ฐ€์„๊นŒ์ง€ 1
959
- ํ’€๋ฐญ์ด๋‚˜ 1
960
- ์„œ์‹ํ•œ๋‹ค 1
961
- ์ƒ๊ฒผ์œผ๋ฉด 1
962
- ๊ณ ๋ ค๋Œ€๊ตฌ๋กœ๋ณ‘์› 1
963
- ์Œ€์Œ€ํ•ด์ง€๋ฉด์„œ 1
964
- ๋ฐ€์ ‘ํ•ด์ง€๋ฉด์„œ 1
965
- ์œ ํ–‰ํ•˜๋Š”๋ฐ 1
966
- ์œ ํ–‰ํ•˜๋˜ 1
967
- ๋งŒ์„ฑ์งˆํ™˜์ž๋“ค 1
968
- ๊ธˆ์œตํˆฌ์ž์†Œ๋“์„ธ 1
969
- ์œ ์˜ˆํ•ด์•ผ 1
970
- ๊ฒƒ์ด์—ˆ์ง€ 1
971
- ๊ณ ๋ คํ•˜๋‹ค 1
972
- ์œ ์˜ˆ๊นŒ์ง€ 1
973
- ์‹œํ–‰ํ•˜์ž 1
974
- ์œ ์˜ˆํ•˜๊ณ  1
975
- ์„ ์ง„์ง€์ˆ˜ 1
976
- ํŽธ์ž…์ด๋ผ๋“ ์ง€ 1
977
- ํŒ๋‹จํ•˜์ž 1
978
- ์œ ์˜ˆํ•˜์ž 1
979
- ์ •์ƒํ™”๋  1
980
- ์•Š๋Š”๋‹ค๋ฉด 1
981
- ๋งˆ์ฐฌ๊ฐ€์ง€๋ผ 1
982
- ์ผ๊ฐ์—์„œ 1
983
- ๊ฐ‘๋ก ์„๋ฐ•ํ•˜๊ธฐ๋ณด๋‹ค 1
984
- ํ์ง€ํ•˜์ž 1
985
- ์œ ์˜ˆํ•œ๋‹ค๋ฉด 1
986
- ์ง‘๊ถŒํ•ด์„œ 1
987
- ์‹œํ–‰ํ•ด์•ผ 1
988
- ์˜ˆ์ •์ด๋˜ 1
989
- ๋ฏธ๋ค„์กŒ๊ณ  1
990
- ์˜์ด์—์„œ 1
991
- ์œ„์ž„ํ•˜๊ธฐ๋กœ 1
992
- ๊ธฐ์šธ์–ด์กŒ๋Š”๋ฐ 1
993
- ๋ฌด๋„ˆ๋œจ๋ฆด 1
994
- ์‹ ์ค‘ํ•ด์•ผ 1
995
- ๋ถ€๋‹ด์Šค๋Ÿฝ๋‹ค 1
996
- ํ‚คํ”„๋กœ์Šค๊ณ„ 1
997
- ๊ทธ๋ฆฌ์Šค์ธ 1
998
- ์‹ฑ๊ฐ€ํฌ๋ฅด์ธ 1
999
- ์‹œ์ž‘ํ–ˆ๊ณ  1
1000
- ๋ณด๋“œ๊ฒŒ์ž„ 1
1001
- ๋›ฐ์–ด๋‚˜๋‹ค 1
1002
- ์ƒ๊ธˆ์œผ๋กœ 1
1003
- ๊ตฌ์ž…ํ•˜๊ณ  1
1004
- ํ”„๋กœ๊ทธ๋ž˜๋ฐ 1
1005
- ๋Œ€ํ•™์—์„œ 1
1006
- ์ปดํ“จํ„ฐ๊ณผํ•™ 1
1007
- ์œ ๋‹ˆ๋ฒ„์‹œํ‹ฐ 1
1008
- ์‹ ๊ฒฝ๊ณผํ•™ 1
1009
- ๋ฐ•์‚ฌํ•™์œ„ 1
1010
- ๊ธฐ์–ต์ƒ์‹ค์ฆ 1
1011
- ์‚ฌ์ด์–ธ์Šค์ง€ 1
1012
- ๋‘๋‡Œ๊ฒŒ์ž„ 1
1013
- ์˜ฌ๋ฆผํ”ผ์•„๋“œ์ธ 1
1014
- ์˜ฌ๋ฆผํ”ผ์•„๋“œ 1
1015
- ๋Ÿฐ๋˜์—์„œ 1
1016
- ๋ฌด์Šคํƒ€ํŒŒ 1
1017
- ์А๋ ˆ์ด๋งŒ 1
1018
- ์Šคํƒ€ํŠธ์—…์ธ 1
1019
- ์ธ์ˆ˜ํ–ˆ์ง€ 1
1020
- ์œ„ํ—˜์ฒ˜๋Ÿผ 1
1021
- ๋ฌด์ฃผํƒ์œผ๋กœ 1
1022
- ๊ฐ•๋‚จ๊ถŒ์—์„  1
1023
- ์ปคํŠธ๋ผ์ธ 1
1024
- ๊ฐ€์ ๋ณด๋‹ค 1
1025
- ๋ฆฌ์–ผํˆฌ๋ฐ์ด 1
1026
- ๋งŒ์ ์ด๋ฉฐ 1
1027
- ๋ถ€์–‘๊ฐ€์กฑ 1
1028
- ์‚ฐ์ •๋œ๋‹ค 1
1029
- ์›ƒ๋Œ์•˜๋‹ค 1
1030
- ๊ณต๊ณต๊ธฐ๊ด€ 1
1031
- ๊ธฐ๊ด€์žฅ์—๊ฒŒ 1
1032
- ๊ทผ์ ˆ๋˜์ง€ 1
1033
- ํ•œ๊ตญ๋„๋ฐ•๋ฌธ์ œ์˜ˆ๋ฐฉ์น˜์œ ์› 1
1034
- ๋“œ๋Ÿฌ๋‚ฌ๋‹ค 1
1035
- ๋ฌธํ™”์ฒด์œก๊ด€๊ด‘์œ„์›ํšŒ 1
1036
- ํ•ด์ž„๋๋‹ค 1
1037
- ๋ช…๋ชฉ์œผ๋กœ 1
1038
- ๋ฒ•๋ฅ ์ž๋ฌธ 1
1039
- ๊ตญ๊ฐ์—์„œ 1
1040
- ์นด์ง€๋…ธ๊ณต๊ธฐ์—…์ธ 1
1041
- ์ง€์ ๋˜์—ˆ์Œ 1
1042
- ๊ฐ€๋Šฅํ•˜๋”๋ผ 1
1043
- ํ•ด์ž„๋˜์—ˆ๋Š”๋ฐ 1
1044
- ์„ธํฌ์—์„œ 1
1045
- ๋Œ์•„๊ฐ”๋‹ค 1
1046
- ์ด๋ค„์กŒ๋‹ค 1
1047
- ์นด๋กค๋ฆฐ์Šค์นด 1
1048
- ์ƒ๋ฆฌ์˜ํ•™์ƒ 1
1049
- ์ˆ˜์ƒ์ž๋กœ 1
1050
- ์ƒ๋ฌผํ•™์ž์ธ 1
1051
- ๋งค์‚ฌ์ถ”์„ธ์ธ  1
1052
- ๋น„๋ฐ€์—ด์‡ ๋กœ 1
1053
- ๋ฐœ๊ฒฌํ•˜์—ฌ 1
1054
- ๊ธฐ์—ฌํ–ˆ๊ธฐ 1
1055
- ์ˆ˜์ƒํ•˜๊ฒŒ 1
1056
- ์ˆ˜์ƒ์ž์ธ 1
1057
- ์•ฐ๋ธŒ๋กœ์Šค 1
1058
- ๋ฏธ์ƒ๋ฌผ์ธ 1
1059
- ์˜ˆ์œ๊ผฌ๋งˆ์„ ์ถฉ 1
1060
- ๋ฐœ๊ฒฌํ–ˆ๋‹ค 1
1061
- ๋น„์ •์ƒ์ ์œผ๋กœ 1
1062
- ์ž‘๋™๋˜๋ฉด 1
1063
- ์ž๊ฐ€๋ฉด์—ญ 1
1064
- ๋ฐœ๊ฒฌ์œผ๋กœ 1
1065
- ์ดํ•ดํ•˜๊ฒŒ 1
1066
- ์กฐ์ ˆ์ด๋ผ 1
1067
- ๋ฌผ๋ฆฌํ•™์ƒ 1
1068
- ๋ฐœํ‘œ๋˜๋ฉฐ 1
1069
- ๊ฒฝ์ œํ•™์ƒ์œผ๋กœ 1
1070
- ์ „์—…ํˆฌ์ž์ž๋‹ค 1
1071
- ๊ฐ•์—ฐ์—์„  1
1072
- ๋ถˆํ•ฉ๋ฆฌํ•œ 1
1073
- ์ฃผ์‹์œผ๋กœ 1
1074
- ๋ฌด์—‡์ธ์ง€ 1
1075
- ๋“ค์–ด๋ณด์ž 1
1076
- ํ™”์š”์ผ์ธ 1
1077
- ํ‡ด์ง๊ธ‰์—ฌ 1
1078
- ์—ฐ๊ธˆ์œผ๋กœ 1
1079
- ์†Œ๋“์„ธ๋ฒ•์ƒ 1
1080
- ์—ฐ๊ธˆ์ˆ˜๋ น์•ก 1
1081
- ํ‡ด์ง์†Œ๋“์„ธ์œจ 1
1082
- ๊ณผ์„ธํ•˜๊ณ  1
1083
- ๋„˜์–ด๊ฐ€๋ฉด์„œ๋ถ€ํ„ฐ 1
1084
- ๊ณผ์„ธํ•œ๋‹ค 1
1085
- ์œ ๋ฆฌํ•œ์ง€ 1
1086
- ๋ฏธ๋ž˜์—์…‹ 1
1087
- ํˆฌ์ž์™€์—ฐ๊ธˆ์„ผํ„ฐ 1
1088
- ๋ถ€์™€์ง€์‹์˜๋ฐฐ์›€ํ„ฐ 1
1089
- ์˜์ƒ์ด๋‹ค 1
1090
- ๋‚ด๋‹ค๋ณด๋ฉด์„œ 1
1091
- ์›€์ง์—ฌ์„œ 1
1092
- ํ”„๋ฆฌ์ฆ˜ํˆฌ์ž์ž๋ฌธ 1
1093
- ๋‹จํ–‰ํ•˜๋ฉด์„œ 1
1094
- ๋ณธ๊ฒฉ์ ์ธ 1
1095
- ์ž์‚ฐ์‹œ์žฅ 1
1096
- ์œ ๋งํ• ์ง€ 1
1097
- ๊ตญ๋ฏผ์—ฐ๊ธˆ๊ณต๋‹จ 1
1098
- ๊ธฐ๊ธˆ์šด์šฉ๋ณธ๋ถ€ 1
1099
- ํˆฌ์ž์šด์šฉํŒ€์žฅ 1
1100
- ๊ตญ๋ฏผ์€ํ–‰ 1
1101
- ์ˆ˜์„์ด์ฝ”๋…ธ๋ฏธ์ŠคํŠธ 1
1102
- ์ „๋ฌธ๊ฐ€๋‹ค 1
1103
- ์‹ ๊ณต์ •์œผ๋กœ 1
1104
- ๋ชจ๋ฐ”์ผ์šฉ 1
1105
- ์ถœ์‹œ๋˜๊ณ  1
1106
- ์•„์ดํฐ์šฉ 1
1107
- ๋Œ์–ด์˜ฌ๋ฆฐ 1
1108
- ํƒ‘์žฌ๋˜๋ฉด์„œ 1
1109
- ์ €์ „๋ ฅ์œผ๋กœ 1
1110
- ์ค‘์š”ํ•ด์กŒ๋‹ค 1
1111
- ๊ฐ•์กฐํ•˜๊ณ  1
1112
- ์ „๋ ฅํšจ์œจ 1
1113
- ํ–ฅ์ƒ๋๋‹ค 1
1114
- ์•ฝ์ ์œผ๋กœ 1
1115
- ๊ฑฐ๋ก ๋˜๋˜ 1
1116
- ๊ฐ•ํ™”๋๋‹ค 1
1117
- ์ „๋ง๋œ๋‹ค 1
1118
- ์ œ์™ธํ•˜๊ณ  1
1119
- ์•ˆ๋“œ๋กœ์ด๋“œ 1
1120
- ์Šค๋งˆํŠธํฐ๋“ค 1
1121
- ํ–ฅ์ƒ๋˜๋ฉด์„œ 1
1122
- ๋–จ์–ด์ง€๋ฉด 1
1123
- ๊ณ ๊ฐ์‚ฌ๋“ค 1
1124
- ๋“ค์–ด๊ฐ€๋ฉด์„œ 1
1125
- ์ข‹์•„์ง€๊ณ  1
1126
- ๊ณ ๋ถ€๊ฐ€๊ฐ€์น˜ 1
1127
- ์ง‘์ค‘ํ•˜๋ฉด์„œ 1
1128
- ์›”๊ฐ€์—์„œ 1
1129
- ๋™๊ฐ‘๋‚ด๊ธฐ๋กœ 1
1130
- ๊ตญ๊ฐ€๋Œ€ํ‘œํŒ€ 1
1131
- ๋„ค๋œ๋ž€๋“œ 1
1132
- ์„ธ๋ฅด๋น„์•„ 1
1133
- ์ฆˆ๋ฒ ์ฆˆ๋‹ค 1
1134
- ํŽ˜์˜ˆ๋…ธ๋ฅดํŠธ๋กœ 1
1135
- ์ฑ”ํ”ผ์–ธ์Šค๋ฆฌ๊ทธ 1
1136
- ๋ฏธ๋“œํ•„๋”๋กœ 1
1137
- ๋งนํ™œ์•ฝํ•˜๋ฉฐ 1
1138
- ๋ถ„๋ฐ์Šค๋ฆฌ 1
1139
- ํ”„๋ž‘ํฌํ‘ธ๋ฅดํŠธ 1
1140
- ์ธก๋ฉด์—์„œ 1
1141
- ์˜ค๋ฅธ๋ฐœ๋กœ 1
1142
- ๋งˆ๋ฌด๋ฆฌํ•ด 1
1143
- ์ฃผ๋‹ˆ์˜ค๋ฅด 1
1144
- ํ—ˆ์šฉํ•˜๋ฉฐ 1
1145
- ๋ฏธ๋“œํ•„๋” 1
1146
- ๋ถ€์ƒ์œผ๋กœ 1
1147
- ๋น ์ง€๋ฉด์„œ 1
1148
- ์†Œ์†ํŒ€์œผ๋กœ 1
1149
- ํŒŒํŠธ๋„ˆ๋กœ 1
1150
- ์ˆ˜๋น„์ˆ˜๋กœ 1
1151
- ์ด๋ค˜๋Š”๋ฐ 1
1152
- ๋Œ€ํ‘œํŒ€์—” 1
1153
- ํฌํ•จ๋๋‹ค 1
1154
- ์ž…์„ฑํ–ˆ๋‹ค 1
1155
- ์ค‘๋™์—์„œ 1
1156
- ์š”๋ฅด๋‹จ์œผ๋กœ 1
1157
- ํ•ฉ๋ฅ˜ํ•œ๋‹ค 1
1158
- ๊ฐœ์ฒœ์ ˆ์ธ 1
1159
- ๋งŒ์„์ด์—ˆ๋‹ค 1
1160
- ์†๋‹˜์œผ๋กœ 1
1161
- ๊ทผ์ฒ˜์—์„œ 1
1162
- ์‡ผํ•‘๊ฐ๋“ค๋กœ 1
1163
- ์‹ ์„ ์‹ํ’ˆ 1
1164
- ๊ณ„์‚ฐ๋Œ€๋งˆ๋‹ค 1
1165
- ๊ธฐ๋‹ค๋ ธ๋‹ค 1
1166
- ๋น„์ˆ˜๊ธฐ๋กœ 1
1167
- ๊ณตํœด์ผ๋กœ 1
1168
- ์ง€์ •๋˜๊ณ  1
1169
- ๋Œ€ํ˜•๋งˆํŠธ๋งˆ๋‹ค 1
1170
- ๋ถ์ ์˜€๊ณ  1
1171
- ์—ฌํ–‰์ง€๋กœ 1
1172
- ๋งค์ง„๋๋‹ค 1
1173
- ์‚ด์•„๋‚˜์ง€ 1
1174
- ์ง„์ž‘ํ•˜๊ธฐ 1
1175
- ์ž„์‹œ๊ณตํœด์ผ 1
1176
- ์†Œ๋น„์‹œ์žฅ 1
1177
- ํšŒ๋ณต๋ ์ง€ 1
1178
- ์œ ํ†ต์—…์ฒด์—์„  1
1179
- ํ’ˆ๋ชฉ์—์„œ 1
1180
- ๋–จ์–ด์ง€๋ฉด์„œ 1
1181
- ์žฅ๋งŒํ•˜๋ ค 1
1182
- ๋กฏ๋ฐ๋ฐฑํ™”์  1
1183
- ํ•ด์™ธ์—ฌํ–‰ 1
1184
- ๋•๋ถ„์ด๋ผ 1
1185
- ์‹ ์„ธ๊ณ„๋ฐฑํ™”์  1
1186
- ๋Š˜์–ด๋‚ฌ๋‹ค๊ณ  1
1187
- ์ฆ‰์„์‹ํ’ˆ 1
1188
- ๋ผ์ง€๊ณ ๊ธฐ 1
1189
- ์—ฌํ–‰๊ฐ๋“ค๋กœ 1
1190
- ์‹ ์ œ์ฃผ์  1
1191
- ์„œ๊ท€ํฌ์  1
1192
- ๊ธฐ๊ฐ„๋ณด๋‹ค 1
1193
- ๋ถˆ๊ฝƒ์ถ•์ œ 1
1194
- ๋ชฐ๋ฆฌ๋ฉด์„œ 1
1195
- ์„œ์šธ์„ธ๊ณ„๋ถˆ๊ฝƒ๏ฟฝ๏ฟฝ๏ฟฝ์ œ 1
1196
- ํ•œ๊ฐ•๊ณต์› 1
1197
- ๋†€์ด๊ณต์› 1
1198
- ํ• ์ธํ–‰์‚ฌ 1
1199
- ๋งค์žฅ์—์„œ 1
1200
- ์ง„ํ–‰ํ•˜๊ณ  1
1201
- ๋กฏ๋ฐ๋งˆํŠธ 1
1202
- ์ƒค์ธ๋จธ์Šค์บฃ 1
1203
- ์ €๋ ดํ•˜๊ฒŒ 1
1204
- ๋จน๊ฑฐ๋ฆฌ๋“ค 1
1205
- ๊ฐœ์ตœํ•œ๋‹ค 1
1206
- ๋‚ด์•ผํ•˜๋‹ˆ 1
1207
- ์œ ํŠœ๋ธŒ๋กœ 1
1208
- ๋ฐœ๋ผ๋ƒˆ๋‹ค 1
1209
- ์ œ์ถœํ•˜์ง€ 1
1210
- ๊ฒฝ๊ณ ๊นŒ์ง€ 1
1211
- ๋ณด๋ƒˆ๋‹ค๊ณ  1
1212
- ์‹œ์ฒญ์ž๋“ค 1
1213
- ์ˆ˜์ต์—์„œ 1
1214
- ๊ธฐ์—ฌ๋ถ„์œผ๋กœ 1
1215
- ๊ธˆ์•ก๋งŒํผ 1
1216
- ์ž…์žฅ์—์„  1
1217
- ์„ธ๋ฌด๋‹น๊ตญ 1
1218
- ๋ฒ•์ธ์ด๋ผ 1
1219
- ์ง•์ˆ˜ํ•˜๊ธฐ 1
1220
- ์‹ค์งˆ์ ์œผ๋กœ 1
1221
- ํ•œ๊ตญ์žฌ๋ฌด๊ด€๋ฆฌํ•™ํšŒ 1
1222
- ๋™๋“ฑํ•˜๊ฒŒ 1
1223
- ๊ณผ์„ธํ•ด์•ผ 1
1224
- ๊ตญ์„ธ๊นŒ์ง€ 1
1225
- ๋…ผ์˜์กฐ์ฐจ 1
1226
- ์›์ฒœ์ง•์ˆ˜ํ•˜๊ธฐ 1
1227
- ๊ฐœ์ •ํ•˜๋ฉด์„œ 1
1228
- ์ •์ฑ…ํ˜‘๋ ฅ 1
1229
- ๋ฐœ์ƒํ•˜๊ณ  1
1230
- ๋‚ฉ๋ถ€ํ•˜๊ฒŒ 1
1231
- ์กฐ์„ธ์กฐ์•ฝ 1
1232
- ๋”ฐ์ ธ๋ณด๊ฒ ๋‹ค 1
1233
- ์ž…์žฅ์ด์—ˆ๋‹ค 1
1234
- ๋“ค์–ด์คฌ๋‹ค 1
1235
- ์‹œ์ฒญ์ž๋“ค๋กœ 1
1236
- ๊ฐ€๋Šฅํ•ด์•ผ 1
1237
- ๊ตฌ๊ธ€์ฝ”๋ฆฌ์•„ 1
1238
- ๋„๋„๋“œ์Šจ 1
1239
- ์ „๋ฌดํ•˜๋‹ค 1
1240
- ์†Œ๋“์„ธ๋ฒ• 1
1241
- ๊ทœ์ •ํ•˜์ง€ 1
1242
- ๊ทœ์ •ํ•˜๋”๋ผ 1
1243
- ์›์ฒœ์†Œ๋“ 1
1244
- ์ดํ–‰ํ•˜์ง€ 1
1245
- ๊ฐ€์†ํ™”๋˜๋ฉด์„œ 1
1246
- ๊ตญ์„ธ์ฒญ์œผ๋กœ 1
1247
- ์ข…ํ•ฉ์†Œ๋“์„ธ 1
1248
- ๋ฒŒ์–ด๋“ค์ธ 1
1249
- ์‹œ์ฒญ์‹œ๊ฐ„ 1
1250
- ๋‹ค๋ฐฉ์ด๋‚˜ 1
1251
- ๊ฐ€์ ธ๊ฐ€๊ฒŒ 1
1252
- ๊ฐ€์ ธ์˜ค๋‹ค 1
1253
- ๋นผ๊ณกํ•˜๊ฒŒ 1
1254
- ์Šต๊ด€์ด์—ˆ๋‹ค 1
1255
- ์ทจ๋ฏธ์ƒํ™œ์ด๋ผ๊ณ  1
1256
- ์œ ํ–‰์ด์—ˆ์–ด์š” 1
1257
- ์ˆ˜์ง‘ํ–ˆ๋‹ค 1
1258
- ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ 1
1259
- ๋์Šต๋‹ˆ๋‹ค 1
1260
- ์ดํ›„๋ผ๊ณ  1
1261
- ๊ธฐ์–ตํ–ˆ๋‹ค 1
1262
- ๋‹ค์ฑ„๋กœ์šด 1
1263
- ๋– ๋“ค์ฉํ•˜๊ฒŒ 1
1264
- ๋งˆ์‹œ๊ฑฐ๋‚˜ 1
1265
- ๊ณ ํ˜•ํ•„์”จ 1
1266
- ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ 1
1267
- ์‚ฌ๋žŒ์—๊ฒŒ 1
1268
- ์‹ธ์›€๊นŒ์ง€ 1
1269
- ํšŒ๊ณ ํ–ˆ๋‹ค 1
1270
- ์‹๋‹น์ด๋‚˜ 1
1271
- ์—ด๊ด‘ํ•˜๊ณ  1
1272
- ์•„๋‚ ๋กœ๊ทธ 1
1273
- ํ˜„๋Œ€์ž๋™์ฐจ๊ทธ๋ฃน 1
1274
- ๋‚œ์–‘๋Œ€ํ•™ 1
1275
- ์ •๋ถ€๊นŒ์ง€ 1
1276
- ๋น„์ฆˆ๋‹ˆ์Šค 1
1277
- ์ฒด๊ฒฐํ–ˆ๋‹ค๊ณ  1
1278
- ๊ณต๊ณผ๋Œ€ํ•™ 1
1279
- ์—ฐ๊ตฌ์ค‘์‹ฌ 1
1280
- ๋Œ€ํ•™์ด๋‹ค 1
1281
- ๋ฐœ์ „์‚ฌ์—… 1
1282
- ์“ฐ๋ ˆ๊ธฐ๋‚˜ 1
1283
- ๋ชจ๋นŒ๋ฆฌํ‹ฐ 1
1284
- ๋ฐธ๋ฅ˜์ฒด์ธ 1
1285
- ์„ค๋ฆฝํ•˜๊ธฐ๋กœ 1
1286
- ํ†ต์ƒ์‚ฐ์—…๋ถ€ 1
1287
- ์กฐ์ง์œผ๋กœ 1
1288
- ์—ฐ๊ตฌ๊ธฐ๊ด€ 1
1289
- ๋กœ๋ณดํ‹ฑ์Šค 1
1290
- ํ˜์‹ ์ œ์กฐ 1
1291
- ๊ตญ๊ฐ€์—ฐ๊ตฌํŽ€๋“œ 1
1292
- ๋‹ค๊ฐ€์™”์ง€ 1
1293
- ๋ถˆํˆฌ๋ช…ํ•ด 1
1294
- ์ตœํ•˜์œ„๊ถŒ 1
1295
- ๊ธˆํˆฌ์„ธ๋ž€ 1
1296
- ๊ธˆ์œต์ƒํ’ˆ 1
1297
- ์–‘๋„์†Œ๋“ 1
1298
- ๋„์ž…๋˜๋ฉด 1
1299
- ์–ด๋ ค์›Œ์งˆ 1
1300
- ๋น ์ ธ๋‚˜๊ฐ€๋ฉด์„œ 1
1301
- ํ•œ๊ตญ๊ธฐ์—…๊ฑฐ๋ฒ„๋„Œ์Šคํฌ๋Ÿผ 1
1302
- ๊ฒฐ๊ณผ์ ์œผ๋กœ 1
1303
- ๋น ์ ธ๋‚˜๊ฐˆ 1
1304
- ๊ด€์ธกํ•˜๊ณ  1
1305
- ๋ถ€์ž๊ฐ์„ธ 1
1306
- ํˆฌ์ž์ž๋“ค์—๊ฒŒ 1
1307
- ํฌ๋ง์ด์ž 1
1308
- ๊ธˆํˆฌ์„ธ๋กœ 1
1309
- ๊ธˆ์œต์†Œ๋“์ข…ํ•ฉ๊ณผ์„ธ๋กœ 1
1310
- ์‹œํ–‰์œผ๋กœ 1
1311
- ๋‚ฎ์•„์ง„๋‹ค 1
1312
- ๋ฒ•์ œ์‚ฌ๋ฒ•์œ„์›ํšŒ 1
1313
- ์ฆ์ธ์œผ๋กœ 1
1314
- ์•„ํŠธ์„ผํ„ฐ 1
1315
- ๋™์•„์‹œ์•„๋ฌธํ™”์„ผํ„ฐ 1
1316
- ์š”๊ตฌํ•˜๊ธฐ๋กœ 1
1317
- ๋ฒ•์‚ฌ์œ„์›์žฅ์ธ 1
1318
- ์†Œ์žฌํŒŒ์•… 1
1319
- ๋™ํ–‰๋ช…๋ น์žฅ 1
1320
- ๋ฐฉ์•ˆ๊นŒ์ง€ 1
1321
- ์•„๋‚ด์ด์ž 1
1322
- ๋– ์˜ฌ๋ž๋‹ค 1
1323
- ์•ฝ์†์–ด์Œ 1
1324
- ์ œ์‹œํ•˜๋ฉฐ 1
1325
- ์ธ์ •๋œ๋‹ค๋ฉด์„œ 1
1326
- ๋ถ€์ธํ•˜๊ณ  1
1327
- ๊ตญ๋ฉด์œผ๋กœ 1
1328
- ์–‘์ƒ์ด๋‹ค 1
1329
- ๋†ํ˜‘์ค‘์•™ํšŒ 1
1330
- ์ ๋ฐœ๋˜์ž 1
1331
- ์ฃผ์‹๊ฑฐ๋ž˜ 1
1332
- ์ ๋ฐœ๋์ง€ 1
1333
- ์ด๋ค„์ง€์ง€ 1
1334
- ์‚ดํŽด๋ณด๊ณ  1
1335
- ๋ฐœ์˜ํ•˜์ž 1
1336
- ์ฒ˜๋ฆฌํ–ˆ์ง€ 1
1337
- ๊ฐ€๋กœ๋ง‰ํ˜”๋‹ค 1
1338
- ๋Œ์–ด์˜ฌ๋ฆฌ๋ ค 1
1339
- ๊ณต์ง์„ ๊ฑฐ๋ฒ• 1
1340
- ๋‹ค๊ฐ€์˜ค๋ฉด์„œ 1
1341
- ์••๋ฐ•ํ•˜๊ธฐ 1
1342
- ์žฌ๋ฐœ์˜ํ–ˆ๋˜ 1
1343
- ๊ฒƒ๋“ค์ด๋‹ค 1
1344
- ์˜๊ฒฐํ•˜๋ฉด 1
1345
- ์ œ์ •ํ•˜์ง€ 1
1346
- ์†Œ์†๋˜๊ฑฐ๋‚˜ 1
1347
- ์†Œ์†๋๋˜ 1
1348
- ๊ตญํšŒ๊ทœ์น™ 1
1349
- ๊ตฌ์„ฑํ•ด์•ผ 1
1350
- ์ž…๋ง›๋Œ€๋กœ 1
1351
- ์ž„๋ช…ํ•˜๊ธฐ 1
1352
- ๋ฐ”๊พธ๊ฒ ๋‹ค 1
1353
- ๋А์Šจํ•ด์ง„ 1
1354
- ๊ฒฐ๊ณผ๋ผ๊ณ  1
1355
- ๋‚ด๋ถ€์ ์œผ๋กœ 1
1356
- ์‹ฌํ™”ํ–ˆ๋‹ค 1
1357
- ๊ณต๊ณต์—ฐํžˆ 1
1358
- ์ž๋ž‘์ด์—ˆ์ง€ 1
1359
- ์˜…์–ด์ง€๊ณ  1
1360
- ์œ ์ผํ•˜๊ฒŒ 1
1361
- ์šฐ์„ ์ˆœ์œ„๊ณ  1
1362
- ์ œ์กฐ์—…์ธ 1
1363
- ์ฒญ๋…„์—๊ฒŒ 1
1364
- ํ•™๊ณผ๊นŒ์ง€ 1
1365
- ์ „์ž๊ณตํ•™ 1
1366
- ์ „๊ณต์ž๋“ค 1
1367
- ํ•™๊ต์—์„œ 1
1368
- ๋™๊ฒฐ๋˜๋ฉด์„œ 1
1369
- ๋”ฐ๋ผ์žก๊ฒ ๋‹ค๊ณ  1
1370
- ์กด์žฌํ•˜์ง€ 1
1371
- ์ตœ์ƒ์ธต๋ถ€ 1
1372
- ์žฅ์•…ํ•˜๊ณ  1
1373
- ํˆฌ์ž๋ณด๋‹ค 1
1374
- ๋ฐ”๋€Œ์—ˆ๊ณ  1
1375
- ์ž„์ง์›๋“ค 1
1376
- ํ•ด๊ฒฐํ• ์ง€ 1
1377
- ์Ÿ์•˜๋Š”๋ฐ 1
1378
- ์ž˜๋ชป์ธ์ง€ 1
1379
- ์ƒ‰์ถœํ•ด๋‚ด์„œ 1
1380
- ์ด๊ธฐ์ฃผ์˜ 1
1381
- ํ„ธ์–ด๋†จ๋‹ค 1
1382
- ์‚ผ์„ฑ๊ทธ๋ฃน 1
1383
- ์ถœ์„ํ•˜๊ณ  1
1384
- ์š”๊ตฌํ–ˆ๊ณ  1
1385
- ๋ถ€๋žด๋ถ€๋žด 1
1386
- ์‹œ์Šคํ…œ๋ฐ˜๋„์ฒด 1
1387
- ๊ฒฝ์Ÿ์‚ฌ์ธ 1
1388
- ์ž๊ทนํ–ˆ๊ณ  1
1389
- ๋А์Šจํ•ด์กŒ๋‹ค 1
1390
- ๊ณผ๋„ํ•˜๊ฒŒ 1
1391
- ๋ฌด์ž„์Šน์ฐจํ•˜๋ฉฐ 1
1392
- ๊ฒฝ์Ÿ์ž์ธ 1
1393
- ์‹œ๊ฐ€์ด์•ก์œผ๋กœ 1
1394
- ๋„˜์–ด์„ฐ๋‹ค 1
1395
- ์ฐฝ์—…์ž์ธ 1
1396
- ์นœ๋ถ„์œผ๋กœ 1
1397
- ํŒŒ์šด๋“œ๋ฆฌ 1
1398
- ์ „๋…ํ–ˆ๋‹ค 1
1399
- ๋นผ๋‹ฎ์•˜๋‹ค 1
1400
- ์žฌ๊ณ„์—์„œ 1
1401
- ์ •์‹ ์œผ๋กœ 1
1402
- ์ถ”์›”ํ•˜๊ณ  1
1403
- ์ค‘์š”ํ•˜๋‹ค 1
1404
- ํ•œ๋ฆผ๋Œ€ํ•™๊ต 1
1405
- ๋™ํƒ„์„ฑ์‹ฌ๋ณ‘์› 1
1406
- ๋Œ€ํ‘œ์ ์ธ 1
1407
- ํ‰๊ฐ€ํ•˜๋ฉฐ 1
1408
- ์ธ์ง€๊ธฐ๋Šฅ๊ณผ 1
1409
- ์ธ์ง€๊ธฐ๋Šฅ 1
1410
- ๋งฅ๋ฝ์ •๋ณด 1
1411
- ํ˜ˆ๊ด€์งˆํ™˜ 1
1412
- ์‹ ์ฒดํ™œ๋™ 1
1413
- ์ง‘๋‹จ๋ณด๋‹ค 1
1414
- ์œ ์ „์ž์ธ 1
1415
- ๋ฒŒ์–ด์กŒ๋‹ค 1
1416
- ํ•œ๋ฆผ๋Œ€๋™ํƒ„์„ฑ์‹ฌ๋ณ‘์› 1
1417
- ์ •์‹ ๊ฑด๊ฐ•์˜ํ•™ 1
1418
- ์ด‰์ง„ํ•˜๊ณ  1
1419
- ๊ธ์ •์ ์ธ 1
1420
- ์—๋””์Šค์ฝ”์™„๋Œ€ํ•™ 1
1421
- ์‚ฌ๋žŒ๋ณด๋‹ค 1
1422
- ์•„๋ฐ€๋กœ์ด๋“œ 1
1423
- ๋ชฉ์ ์—์„œ 1
1424
- ์‘๋‹ต์ž๋“ค 1
1425
- ์—ฐ๋ น๋ณ„๋กœ 1
1426
- ๋‹ค๊ฐ€์˜ฌ์ˆ˜๋ก 1
1427
- ๊ธฐํ˜ผ์ž๋“ค๋ณด๋‹ค 1
1428
- ๋Œ€๋น„ํ•ด์•ผ 1
1429
- ๊ฒฐ๊ณผ์—์„  1
1430
- ์ง์žฅ์ธ๋ณด๋‹ค 1
1431
- ์ €์ถ•ํ•˜๊ณ  1
1432
- ์ฒด๊ณ„์ ์ธ 1
1433
- ๋ฏธ์นœ๋‹ค๊ณ  1
1434
- ๋‹จ๊ธฐ์ ์ธ 1
1435
- ์ง‘์ค‘ํ•ด์„œ 1
1436
- ์ง€์ ์ด๋‹ค 1
1437
- ๋‚ฉ์ž…ํ•œ๋‹ค 1
1438
- ์„ธ์•ก๊ณต์ œ 1
1439
- ์ค‘๊ตญ์œผ๋กœ 1
1440
- ํ™•๋ณดํ•˜๊ธฐ 1
1441
- ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค 1
1442
- ๊ธฐ์ˆ ์ ์ธ 1
1443
- ์˜คํ”ˆ์†Œ์Šค 1
1444
- ๋ณธ์‚ฌ์—์„œ 1
1445
- ๋ถ€์‚ฌ์žฅ์œผ๋กœ 1
1446
- ๊ทผ๋ฌดํ–ˆ๊ณ  1
1447
- ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ 1
1448
- ๊ณผํ•™์ž๋กœ 1
1449
- ์„ ๋„ํ•˜๊ณ  1
1450
- ์˜คํ”ˆ์†Œ์Šค๋กœ 1
1451
- ํ˜์‹ ์ ์ธ 1
1452
- ํ”„๋ž‘์Šค๋ฟ 1
1453
- ์ถœ์‹ ๋“ค๋กœ 1
1454
- ์ „์Ÿ์ด๋‹ค 1
1455
- ์ถฉ๋ถ„ํ•˜์ง€ 1
1456
- ์กฐ์ง์—๊ฒŒ 1
1457
- ์น˜์—ดํ•ด์ง€๊ณ  1
1458
- ํšจ์œจ์ ์ธ 1
1459
- ์•„ํ‚คํ…์ฒ˜ 1
1460
- ์ž์›์œผ๋กœ 1
1461
- ๊ฐœ๋ฐœํ•ด์•ผ 1
1462
- ์ œ๊ณต์ž๋“ค๊ณผ 1
1463
- ํŒŒํŠธ๋„ˆ์‹ญ 1
1464
- ์œ ๋Ÿฝ์œผ๋กœ 1
1465
- ํ™•๋Œ€ํ•ด๋‚˜๊ฐ„๋‹ค 1
1466
- ๊ฒฝ์ œ์—์„œ 1
1467
- ๋งค์ถœ์—์„œ 1
1468
- ์ฐจ์ง€ํ•œ๋‹ค 1
1469
- ์ˆ˜์ถœ์—์„œ 1
1470
- ๋‘๋“œ๋Ÿฌ์ง„๋‹ค 1
1471
- ๋ฐ˜๋„์ฒด๊ณ  1
1472
- ์ƒํ™œ๊ฐ€์ „ 1
1473
- ์‚ด๋ฆผ์‚ด์ด 1
1474
- ์ขŒ์šฐํ•œ๋‹ค 1
1475
- ๋‚ฉ๋ถ€ํ–ˆ๋‹ค 1
1476
- ์œ ์ง€ํ•ด์™”๋‹ค 1
1477
- ๊ณต์ค‘์—์„œ 1
1478
- ๋‚˜ํ’€๊ฑฐ๋ฆฌ๊ณ  1
1479
- ํšŒ์ „ํ•œ๋‹ค 1
1480
- ๊ณต๊ฐ„์œผ๋กœ 1
1481
- ์žฌํƒ„์ƒํ–ˆ๋‹ค 1
1482
- ํ˜„๋Œ€๋ฏธ์ˆ ๊ด€ 1
1483
- ์ž‘๊ฐ€์ด์ž 1
1484
- ๋ถ€๋ฅด์ฃผ์•„ 1
1485
- ์˜ฌ๋ผํ‘ธ๋ฅด 1
1486
- ์—˜๋ฆฌ์•„์† 1
1487
- ์•„์ด์›จ์ด์›จ 1
1488
- ๋‚ด๋กœ๋ผํ•˜ 1
1489
- ํ˜„๋Œ€๋ฏธ์ˆ  1
1490
- ๊ฑฐ์ณ๊ฐ”๋‹ค 1
1491
- ํ˜„๋Œ€์ž๋™์ฐจ 1
1492
- ์ง€์›์œผ๋กœ 1
1493
- ๋А๊ปด์ง€์ง€ 1
1494
- ์•Š์•˜๋А๋ƒ 1
1495
- ์งˆ๋ฌธ์ธ๋ฐ 1
1496
- ๋ฌด์„œ์›Œํ•˜๊ธฐ 1
1497
- ์•„๋ฆ„๋‹ค์›€ 1
1498
- ๊ณต์กดํ•œ๋‹ค 1
1499
- ๋ฐ”๊พธ๋ฉด์„œ 1
1500
- ๋–จ์–ด๋œจ๋ฆฐ๋‹ค 1
1501
- ๋ฐœ์ „์†Œ์˜€๋˜ 1
1502
- ์ฃผ๋ชฉํ–ˆ๋‹ค 1
1503
- ์‚ฌ์šฉํ•˜์ง€ 1
1504
- ์žฌ๊ฐ€๋™ํ–ˆ๊ณ  1
1505
- ํ˜„์žฅ์œผ๋กœ 1
1506
- ํƒˆ๋ฐ”๊ฟˆ์‹œ์ผฐ๋‹ค 1
1507
- ์žฌ๊ฐ€๋™ํ•ด 1
1508
- ์žฅ์†Œ์˜€๋‹ค 1
1509
- ์†Œ๊ฐœํ–ˆ๋‹ค 1
1510
- ๋Œ์–ด์™”๋‹ค 1
1511
- ์กฐ๊ฐ์œผ๋กœ 1
1512
- ๋งค๋‹ฌ๋ฆฌ๊ฒŒ 1
1513
- ์ถ”๊ฐ€๋˜๋ฉด์„œ 1
1514
- ๋“ค์–ด๊ฐ€๊ธฐ 1
1515
- ํšกํฌ๋ผ๊ณ  1
1516
- ์ƒ์ฒ˜์—์„œ 1
1517
- ๋ฌด๊ธฐ๋ ฅํ•จ 1
1518
- ์•„๋ฆ„๋‹ต๋‹ค 1
1519
- ๊ฐ๋™ํ•œ๋‹ค 1
1520
- ์›€์ง์ธ๋‹ค 1
1521
- ์—ญ๊ฒฝ์ด๋‚˜ 1
1522
- ์•„๋ฆ„๋‹ต๋‹ค๊ณ  1
1523
- ๋“ค์–ด์„œ๋ฉฐ 1
1524
- ๊ณต๊ฐ„์ด๋‹ค 1
1525
- ํ๋ ˆ์ดํ„ฐ 1
1526
- ๋ถ€๋“œ๋Ÿฌ์›€ 1
1527
- ๊ฐ๊ฐ์ ์œผ๋กœ 1
1528
- ๊ฒฝํ—˜ํ•˜๊ฒŒ 1
1529
- ์˜ˆ๋ฐฉํ•˜๊ธฐ 1
1530
- ์†์งˆํ•ด์•ผ 1
1531
- ๊ตญํ† ๊ตํ†ต๋ถ€ 1
1532
- ํ•˜์ž์‹ฌ์‚ฌ๋ถ„์Ÿ์กฐ์ •์œ„์›ํšŒ 1
1533
- ์œก๋ฐ•ํ–ˆ๋‹ค 1
1534
- ์ฆ๊ฐ€ํ•˜๋ฉด์„œ 1
1535
- ์ฒ˜๋ฆฌํ•ด์•ผ 1
1536
- ์‹ค์‹œํ•ด์•ผ 1
1537
- ์ „๋ฌธ๊ฐ€๋กœ 1
1538
- ํ’ˆ์งˆ์ ๊ฒ€๋‹จ 1
1539
- ์ ๊ฒ€ํ•ด์•ผ 1
1540
- ์ž‘๋…„๋ถ€ํ„ฐ 1
1541
- ์ž…์ฃผ์˜ˆ์ •์ž 1
1542
- ๋™ํ–‰ํ•˜์ง€ 1
1543
- ๊ธˆ์ง€ํ–ˆ๋‹ค 1
1544
- ์‚ฌ์ „๋ฐฉ๋ฌธ 1
1545
- ๋ช…์‹œํ•˜๊ธฐ๋กœ 1
1546
- ๋‹ดํ•ฉํ•œ๋‹ค 1
1547
- ์ œ๊ธฐ๋๋‹ค 1
1548
- ์ง„ํ–‰ํ•˜๋ฉด 1
1549
- ์ž…์ฃผ์˜ˆ์ •์ž๋“ค 1
1550
- ์‹ค์‹œํ•˜๋Š”๋ฐ 1
1551
- ๋ณ€๊ฒฝํ–ˆ๋‹ค 1
1552
- ์‚ฌ์ „๋ฐฉ๋ฌธ์ผ 1
1553
- ์ฃผ๊ฐ„๋ณ„๋กœ 1
1554
- ๊ท ๋“ฑํ•˜๊ฒŒ 1
1555
- ์ „์‚ฐ์‹œ์Šคํ…œ 1
1556
- ์ผ๋ฐฉ์ ์œผ๋กœ 1
1557
- ๊ฐœ์„ ํ•ด์•ผ 1
1558
- ์ง€์ž์ฒด๋งˆ๋‹ค 1
1559
- ์ฒœ์ฐจ๋งŒ๋ณ„์ด์—ˆ๋‹ค 1
1560
- ๊ฐ•ํ™”ํ•ด์•ผ 1
1561
- ๋ฐฉ๋ฌธํ•˜๊ณ  1
1562
- ์ฐธ์„ํ•˜๊ธฐ 1
1563
- ์ถœ๊ตญํ–ˆ๋‹ค 1
1564
- ๋ฐฉ๋ฌธ๊ตญ์ธ 1
1565
- ์ฐธ๋ฐฐํ•˜๊ณ  1
1566
- ๋งบ์–ด์™”์œผ๋ฉฐ 1
1567
- ์ตœ์ดˆ์ด์ž 1
1568
- ์ž์œ ๋Œ€ํ•œ๋ฏผ๊ตญ 1
1569
- ์ˆœ๋ฐฉ์—์„œ 1
1570
- ์šฉ์ธํ•˜์ง€ 1
1571
- ๊ฒจ๋ƒฅํ•˜๊ธฐ 1
1572
- ๋ชจ๋‹ˆํ„ฐ๋ง 1
1573
- ํ˜•์„ฑ๋˜์–ด 1
1574
- ์–‘์ƒ์œผ๋กœ 1
1575
- ํ˜๋Ÿฌ๊ฐ€๊ณ  1
1576
- ์˜ˆ์ƒ๋์ง€ 1
1577
- ์—ฌ๋ก ์กฐ์‚ฌ์ƒ 1
1578
- ๋‚˜ํƒ€๋‚˜๋ฉด์„œ 1
1579
- ์ •๊ฐ€์—์„œ 1
1580
- ์˜ˆ์ธกํ•˜๊ธฐ 1
1581
- ๊ฒฐ์ •์œผ๋กœ 1
1582
- ํ†ตํ•ฉ์ง„๋ณด๋‹น 1
1583
- ํ›„์‹ ์œผ๋กœ 1
1584
- ์œ„์„ฑ์ •๋‹น 1
1585
- ํ™•๋ณดํ–ˆ๋‹ค 1
1586
- ๋„ค๊ฑฐํ‹ฐ๋ธŒ 1
1587
- ์•ผ๊ถŒ์—์„  1
1588
- ์ง„๋ณด๋‹น์‹ 1
1589
- ๋‚จ๋„์ผ๋ณด 1
1590
- ์ง€์ง€ํ•œ๋‹ค 1
1591
- ์กฐ์‚ฌ์—์„  1
1592
- ์ผ์ฃผ์ผ์—ฌ 1
1593
- ๋„ค๊ฑฐํ‹ฐ๋ธŒ์ „ 1
1594
- ๋ฒŒ์ด๋ฉด์„œ 1
1595
- ๋‚œํƒ€์ „์‹ 1
1596
- ์ง„๋ณด๋‹น์œผ๋กœ 1
1597
- ํ•™๋„ํ˜ธ๊ตญ๋‹จ 1
1598
- ์ดํ•™์ƒ์žฅ 1
1599
- ๊ณต๊ฒฉํ–ˆ๊ณ  1
1600
- ์ง‘ํšŒ์‹œ์œ„๋ฒ• 1
1601
- ๋ถ€๊ฐ๋˜์ง€ 1
1602
- ๋ฐ€์–ด๋‚ด๊ณ  1
1603
- ๋‹น์„ ๋๋˜ 1
1604
- ์ž๊ทนํ–ˆ๋Š”๋ฐ 1
1605
- ์กฐ์ง์—์„œ 1
1606
- ๋‹น์„ ๋ฌดํšจํ˜• 1
1607
- ํ™•์ •๋˜์ž 1
1608
- ์๋‚ด์—์„  1
1609
- ๋“ค์–ด์ฃผ๊ณ  1
1610
- ๋ถ€์ถ•ํ•ด์ฃผ 1
1611
- ์˜๊ด‘๊ตฐ๋†๋ฏผํšŒ 1
1612
- ์‹œ์ ˆ๋ถ€ํ„ฐ 1
1613
- ์ง‘์ง‘๋งˆ๋‹ค 1
1614
- ๋…ผ๋ฐญ๊นŒ์ง€ 1
1615
- ์š”๊ตฌํ•˜๋ฉฐ 1
1616
- ํฌํ“ฐ๋ฆฌ์ฆ˜ 1
1617
- ๊ฒฝ์Ÿ์œผ๋กœ 1
1618
- ์˜๊ด‘์‚ฌ๋ž‘์ง€์›๊ธˆ 1
1619
- ๊ณต์•ฝ์œผ๋กœ 1
1620
- ํŒ๋ฐ•์ด๋กœ 1
1621
- ์˜๊ด‘๊ตฐ๋…ธ์ธ๋ณต์ง€๊ด€ 1
1622
- ์–ป์–ด๋†“๊ณ  1
1623
- ๋‚ด๋ ค๊ฐ”๊ณ  1
1624
- ์ƒ์ž„๋Œ€ํ‘œ 1
1625
- ์ตœ์—ฐ์†Œ๋‹ค 1
1626
- ์ฃผ์ฃผ์ดํšŒ 1
1627
- ๋Œ€ํ‘œ์ด์‚ฌ 1
1628
- ์„ ์ž„ํ–ˆ๋‹ค 1
1629
- ์ œํ’ˆ์ด๊ด„๋กœ 1
1630
- ์นด๋„ค๊ธฐ๋ฉœ๋ก ๋Œ€ 1
1631
- ์—”๋น„์š˜๋“œ 1
1632
- ์ด๋ฒ ์ด์ฝ”๋ฆฌ์•„ 1
1633
- ๋น„๊ธˆ์œต๊ถŒ 1
1634
- ํšŒ์‚ฌ์—์„œ 1
1635
- ํ•ด์™ธ์ฃผ์‹ 1
1636
- ์ถ”์ง„ํ•˜๋ฉฐ 1
1637
- ํ”Œ๋žซํผ์œผ๋กœ์„œ 1
1638
- ๋Œ€ํ‘œ์ด์‚ฌ๋กœ 1
1639
- ๋ฌผ๋Ÿฌ๋‚ฌ๋‹ค 1
1640
- ๊ธฐ๊ฐ„์œผ๋กœ 1
1641
- ํ•œ์ •ํ•˜๋ฉด 1
1642
- ๊ฐ€์†ํ™”ํ• ์ˆ˜๋ก 1
1643
- ์‹œ๊ฐ„๋™์•ˆ 1
1644
- ๋น„๊ตฌ๋ฆ„๋Œ€ 1
1645
- ์ˆ˜์ฆ๊ธฐ๋Ÿ‰ 1
1646
- ํ† ํ•ด๋‚ด๋“ฏ 1
1647
- ์†์ถœํ–ˆ๋‹ค 1
1648
- ์žฅ๋งˆ๊ธฐ๊ฐ„ 1
1649
- ๋งŽ์•„์ง€๋ฉฐ 1
1650
- ํŽธ์„ฑํ•˜๊ณ  1
1651
- ์šด์˜ํ•˜๊ณ  1
1652
- ๋ณต๊ธฐ์กฐ๋กœ 1
1653
- ์šด์˜๋œ๋‹ค 1
1654
- ์ด์–ด์ง€๊ฒ ๋‹ค 1
1655
- ์Œ€์Œ€ํ•˜๊ณ  1
1656
- ๋ณด์ด๊ฒ ๋‹ค 1
1657
- ์ด์–ด์ง€๊ฒ ๋‹ค๊ณ  1
1658
- ๊ฒฝ๋‚จ์ง€์—ญ 1
1659
- ๋ธŒ๋กœ์ปค๋กœ 1
1660
- ์ˆ ๋ ์ด๊ณ  1
1661
- ๋น„๋Œ€์œ„์›์žฅ 1
1662
- ์–˜๊ธฐํ•˜๋ฉด์„œ 1
1663
- ์ •์น˜์ž๊ธˆ๋ฒ• 1
1664
- ์ฐฝ์›์ง€๊ฒ€ 1
1665
- ๊ตฌ์†ํ•˜๋ฉด 1
1666
- ํ•˜์•ผํ•˜๊ณ  1
1667
- ๊ฐ๋‹น๋˜๊ฒ ๋А๋ƒ๊ณ  1
1668
- ๊ฒ€์‚ฌ์—๊ฒŒ 1
1669
- ์‘ํ•˜๋ฉด์„œ 1
1670
- ๊ณคํ˜น์Šค๋Ÿฌ์›Œํ•˜ 1
1671
- ์ •์น˜๋ธŒ๋กœ์ปค 1
1672
- ๊ตญ๋ฏผ๊ป˜์„œ 1
1673
- ํ•œ์‹ฌํ•˜๊ฒŒ 1
1674
- ์ƒ๊ฐํ•˜์‹ค 1
1675
- ์ˆ˜์‚ฌ๊ธฐ๊ด€ 1
1676
- ์—„์ •ํ•˜๊ณ  1
1677
- ์‹ ์†ํ•˜๊ฒŒ 1
1678
- ์ˆ˜์‚ฌํ•ด์•ผ 1
1679
- ์žํƒ์œผ๋กœ 1
1680
- ์กฐ์–ธ์œผ๋กœ 1
1681
- ํ…”๋ ˆ๊ทธ๋žจ 1
1682
- ๋‚˜ํƒ€๋‚˜๊ณ  1
1683
- ๋ถ€์ ์ ˆํ•œ 1
1684
- ๋ถ€์ •ํ™•ํ•œ 1
1685
- ์„ค๋ช…ํ–ˆ๋Š”๋ฐ 1
1686
- ๋‹น์‚ฌ์ž๋“ค 1
1687
- ๋ฐ˜๋ฐ•ํ–ˆ๋‹ค 1
1688
- ๋งŒ๋‚˜์ž๊ณ  1
1689
- ํŽ˜์ด์Šค๋ถ 1
1690
- ๋น…ํ…Œํฌ๋“ค 1
1691
- ํŒŒ์•…ํ•ด์•ผ 1
1692
- ์œ ๋Ÿฝ์—ฐํ•ฉ 1
1693
- ๋Œ€ํ•ญํ•˜๊ธฐ 1
1694
- ๊ณต๊ฐœํ•˜๋„๋ก 1
1695
- ํšŒ๊ณ„์—ฐ๋„ 1
1696
- ์ข…๋ฃŒ๋˜๋ฉด 1
1697
- ๊ณต๊ฐœํ•ด์•ผ 1
1698
- ๋‹น๊ตญ๋ผ๋ฆฌ 1
1699
- ๊ณต์œ ํ•˜๋˜ 1
1700
- ๊ตญ์ œํ˜‘์•ฝ์ธ 1
1701
- ์ดํ–‰ํ•˜๊ธฐ 1
1702
- ๊ตญ๊ฐ€๋ณ„๋ณด๊ณ ์„œ 1
1703
- ๋„์ž…ํ–ˆ๊ณ  1
1704
- ์ •๋ณด๊ตํ™˜ 1
1705
- ๊ตํ™˜ํ•˜๊ณ  1
1706
- ๋น…ํ…Œํฌ๋กœ 1
1707
- ๊ฐ€๊ณตํ•ด์„œ 1
1708
- ๊ตญ๊ฐ€์—์„œ 1
1709
- ์œ ๋Ÿฝ์ฒ˜๋Ÿผ 1
1710
- ์ถ”์ง„๋˜๊ณ  1
1711
- ๊ณต๊ฐœํ•˜๊ธฐ 1
1712
- ์„ ํ–‰๋ผ์•ผ 1
1713
- ์ƒˆ๋ฒฝ๊นŒ์ง€ 1
1714
- ์„œ์šธ๊ฒฝ์ฐฐ์ฒญ 1
1715
- ํ˜„์žฅ์—์„œ 1
1716
- ์—†์—ˆ์œผ๋ฉฐ 1
1717
- ๊ฑฐ๋ถ€ํ•˜์ง€ 1
1718
- ๋ถ€์ถ•ํ•˜๋ ค 1
1719
- ํฌ์ฐฉ๋˜๊ธฐ 1
1720
- ๋“ค์—ฌ๋‹ค๋ณด๊ณ  1
1721
- ๋ถˆ๊ฐ€๋Šฅํ•œ 1
1722
- ์ด์ „๊นŒ์ง€ 1
1723
- ์ฐจ์„ ์—์„œ 1
1724
- ๋‚œํญ์šด์ „ 1
1725
- ์ ‘์ˆ˜ํ•˜๊ณ  1
1726
- ์ฃผ๋ฌธํ–ˆ๋‹ค 1
1727
- ๊พธ๋ฒ…๊พธ๋ฒ… 1
1728
- ์ฒ ๊ฑฐ๋œ๋‹ค 1
1729
- ๊ฐœํ†ตํ–ˆ๋‹ค 1
1730
- ๊ต์ฐจ๋กœ๋กœ 1
1731
- ๋„๋กœ๊ตํ†ต๊ณต๋‹จ 1
1732
- ๋ถˆ๊ฐ€ํ”ผํ•  1
1733
- ์ฒ ๊ฑฐํ•˜๊ณ  1
1734
- ์กฐ์„ฑํ•œ๋‹ค 1
1735
- ์—ฐ๊ฒฐํ•œ๋‹ค 1
1736
- ์—ฌ์˜๋„๋‚˜ 1
1737
- ๋„์‹ฌ์œผ๋กœ 1
1738
- ์ถœํ‡ด๊ทผํ•˜ 1
1739
- ๋ฐ˜๋„์ฒด๋ฟ 1
1740
- ๋ฐ€์–ด๋‚ด๊ธฐ 1
1741
- ์ง€์ง€๋ถ€์ง„ํ•œ 1
1742
- ์„ ๋‘์ฃผ์ž 1
1743
- ์‹ค์ ์œผ๋กœ 1
1744
- ๊ฐ์†Œํ–ˆ๋Š”๋ฐ 1
1745
- ์˜์—…์ด์ต์œผ๋กœ 1
1746
- ์ œ์™ธํ•˜๋ฉด 1
1747
- ์ ์ž์˜€๋‹ค 1
1748
- ์˜ํ–ฅ์ด๋‹ค 1
1749
- ์ˆ˜์กฐ์›๋Œ€ 1
1750
- ์ฒด๊ฒฐํ–ˆ๋‹ค 1
1751
- ๋”ฐ๋‚ด๋ฉด์„œ 1
1752
- ํฌํŠธํด๋ฆฌ์˜ค 1
1753
- ๋‹ค๋ณ€ํ™”๋กœ 1
1754
- ํŽธ๊ด‘ํ•„๋ฆ„ 1
1755
- ์–‘๋„ํ–ˆ๊ณ  1
1756
- ๊ตฌ์กฐ์กฐ์ • 1
1757
- ์ตœ๋Œ€์น˜์˜€์ง€ 1
1758
- ํ•ด์šด์—์„œ 1
1759
- ์˜ํ–ฅ์œผ๋กœ 1
1760
- ๋ถ„์„๋๋‹ค 1
1761
- ๊ธธ์–ด์ง€๊ณ  1
1762
- ๋น„์ƒ๊ฒฝ์˜ 1
1763
- ๋Œ์ž…ํ–ˆ์ง€ 1
1764
- ๋ฏธ๋‚˜ํ† ๊ตฌ 1
1765
- ํŽœํŠธํ•˜์šฐ์Šค์ด์ž 1
1766
- ์ผ๋ณธ์—์„œ 1
1767
- ๋ ˆ์ง€๋˜์Šค๋ผ 1
1768
- ๋“ค์–ด์„ฐ๋Š”๋ฐ 1
1769
- ํ™”๋ฃก์ ์ • 1
1770
- ๋””์ž์ธํ–ˆ๋‹ค 1
1771
- ์Šค์นด๋‹์œผ๋กœ 1
1772
- ๊ฐ€๊ตฌ์—…์ฒด 1
1773
- ๊พธ๋ช„๋‹ค๊ณ  1
1774
- ์˜ท์žฅ๊นŒ์ง€ 1
1775
- ๋””์ž์ด๋„ˆ๋“ค 1
1776
- ๊ณ ์‹ฌํ•ด์„œ 1
1777
- ์†Œ๊ฐœํ•˜๊ณ  1
1778
- ๊ฐค๋Ÿฌ๋ฆฌ์ŠคํŠธ 1
1779
- ์ปจ์„คํ„ดํŠธ 1
1780
- ๊ฑด์ถ•๊ฐ€๋‚˜ 1
1781
- ์ด์–ด์ง„๋‹ค 1
1782
- ๊ฐค๋Ÿฌ๋ฆฌ์—์„  1
1783
- ๊ฑด์„ค์—…์ฒด 1
1784
- ํˆฌ์žํšŒ์‚ฌ๋“ค 1
1785
- ํ˜„์ƒ๊นŒ์ง€ 1
1786
- ๋ฒŒ์–ด์ง€๊ณ  1
1787
- ์กฐ์—ฐ์ด์—ˆ๋‹ค 1
1788
- ๋จผ์ €์˜€๊ณ  1
1789
- ์ด๋•Œ๋ถ€ํ„ฐ 1
1790
- ์‹œ์ž‘๋๋‹ค 1
1791
- ๊ฐ„๊ฒฐํ•˜๋ฉด์„œ 1
1792
- ์ดˆ๋ถ€ํ„ฐ๋‹ค 1
1793
- ๋Œ€์ค‘๏ฟฝ๏ฟฝ๏ฟฝ์œผ๋กœ 1
1794
- ๋””์ž์ด๋„ˆ๋กœ 1
1795
- ์•คํŠธ์›Œํ”„ 1
1796
- ๊ด€๋Šฅ์ ์œผ๋กœ 1
1797
- ์œˆํŠธ๋ ˆ๋ฒ ๋ฅดํŠธ 1
1798
- ์œ ๊ธฐ์ ์ธ 1
1799
- ๊ฐ๊ด‘๋ฐ›๊ณ  1
1800
- ์–ดํผํ•˜์šฐ์Šค 1
1801
- ๋ฅดํŽ˜์ด๋“œ 1
1802
- ๋‚˜์ธ์›ํ•œ๋‚จ 1
1803
- ์‹ค๋‚ด๊ฐ€๊ตฌ 1
1804
- ๋ฐฐ์น˜ํ•˜๋А๋ƒ 1
1805
- ๋‹ฌ๋ผ์ง„๋‹ค 1
1806
- ์กฐ์‚ฌ๊ธฐ๊ด€ 1
1807
- ํ• ๋ฆฌํŒฉ์Šค 1
1808
- ๊ฑธ๋ ธ๋Š”๋ฐ 1
1809
- ๊ตฌ์กฐ๋งŒํผ 1
1810
- ๋‰ด์š•์ด๋‚˜ 1
1811
- ๋ ˆ์ง€๋˜์Šค์ผ์ˆ˜๋ก 1
1812
- ์ง€์—ญ๋ฏผ๋ณด๋‹จ 1
1813
- ์™ธ๊ตญ์ธ๋“ค 1
1814
- ๊ท€๋”ํ–ˆ๋‹ค 1
1815
- ์•„์ž๋‹ค๋ถ€ํž์Šค 1
1816
- ์ค‘๋™์ด๋‚˜ 1
1817
- ์œ ๋Ÿฝ์—์„œ 1
1818
- ์™ธ๊ตญ์ธ๋“ค์ด๊ณ  1
1819
- ๊ณ ๊ฐ์ด๋ผ 1
1820
- ํ•ํ”Œ๋ ˆ์ด์Šค 1
1821
- ๋งŒ์กฑ์‹œํ‚ฌ 1
1822
- ์ฐจ๋ณ„์ ์ธ 1
1823
- ํ•„์š”ํ•˜๊ณ  1
1824
- ๋””์ž์ธํ•œ 1
1825
- ์„ค๋ช…์ด๋‹ค 1
1826
- ํผ๋‹ˆ์ฒ˜๋กœ 1
1827
- ๋ ˆ์ง€๋˜์Šค๋ผ๊ณ  1
1828
- ํ•˜์ด์—”๋“œ 1
1829
- ์˜คํ”ผ์Šคํ…” 1
1830
- ์ž๊ธˆ๋‚œ์œผ๋กœ 1
1831
- ๊ฐ€๋ผ์•‰์ง€ 1
1832
- ์—ฌ์ „ํ•˜๊ธฐ 1
1833
- ์‹ ์ค‘ํ•˜๊ฒŒ 1
1834
- ๋”ฐ์ ธ๋ณด๊ณ  1
1835
- ์œ ์ถœ๋๋‹ค 1
1836
- ์ฐฉ์ˆ˜ํ•œ๋‹ค๊ณ  1
1837
- ์ธํ„ฐ๋ทฐํ•˜๋ฉด์„œ 1
1838
- ๋น„์ƒ๋Œ€์ฑ…์œ„์›์žฅ 1
1839
- ์‹ค์‹œํ–ˆ๋‹ค๋ฉด์„œ 1
1840
- ์ฑ…์ž„๋‹น์› 1
1841
- ๋ฏธ๋ž˜ํ•œ๊ตญ์—ฐ๊ตฌ์†Œ 1
1842
- ๋น„๊ตํ–ˆ๊ณ  1
1843
- ๊ฐ€์ƒ๋Œ€๊ฒฐ 1
1844
- ์‹ค์‹œํ–ˆ์œผ๋ฉฐ 1
1845
- ์‚ฌ๋ฌด์ด์žฅ 1
1846
- ์ตœ๊ณ ์œ„์› 1
1847
- ํšŒ์˜์—์„œ 1
1848
- ๋งŒ๋“ค์–ด์„œ 1
1849
- ์ ๋ฒ•ํ•˜๊ฒŒ 1
1850
- ํ˜๋Ÿฌ๊ฐ”๋Š”์ง€ 1
1851
- ๋„˜์–ด๊ฐ”๋Š”์ง€ 1
1852
- ์กฐ์‚ฌํ•˜๊ฒ ๋‹ค 1
1853
- ์•ˆ์‹ฌ๋ฒˆํ˜ธ 1
1854
- ์บ ํ”„์—์„œ 1
1855
- ๊ด€๋ จํ•ด์„œ 1
1856
- ๋šœ๋ ทํ•ด์•ผ 1
1857
- ์‹œ๊ฐ„๊ฐ€๋Ÿ‰ 1
1858
- ๊ฒฐ๋ก ์ž…๋‹ˆ๋‹ค 1
1859
- ์„œ๋ฐ”์ด๋ฒŒ 1
1860
- ๋ฏธ์…˜์—์„œ 1
1861
- ์Šน๋ฆฌํ•˜๋ฉฐ 1
1862
- ๋Œ์—ˆ์Šต๋‹ˆ๋‹ค 1
1863
- ํ•ด์‚ฐ๋ฌผ๋กœ 1
1864
- ์‹น์“ธ์ดํ•˜๋ฉฐ 1
1865
- ์…ฐํ”„์—๊ฒŒ 1
1866
- ๋ฌผ์—ˆ๋”๋‹ˆ 1
1867
- ๋ฐฉ์†ก์—์„œ 1
1868
- ์ต์ˆ™ํ•˜๋ฉด์„œ 1
1869
- ๋งŒ๋“ค์–ด์•ผ๊ฒ ๋‹ค 1
1870
- ์ฃผ์ €ํ•˜์ง€ 1
1871
- ๋šœ๋ ทํ–ˆ๊ธฐ 1
1872
- ๋ฆฌ๋”์—๊ฒŒ 1
1873
- ๋‹ค์–‘ํ•ฉ๋‹ˆ๋‹ค 1
1874
- ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค 1
1875
- ์•„๋ฆ„๋‹ค์› ๋‹ค๋ฉด 1
1876
- ์ค‘์š”ํ•˜์ง€ 1
1877
- ์ข‹์Šต๋‹ˆ๋‹ค 1
1878
- ์—ผ๋‘ํ•ด๋‘” 1
1879
- ์•ˆํƒ€๊น์ง€ 1
1880
- ์•„๋‹๊นŒ์š” 1
1881
- ๋„๋กœ๊ตํ†ต๋ฒ•์ƒ 1
1882
- ํŠน์ •๋ฒ”์ฃ„๊ฐ€์ค‘์ฒ˜๋ฒŒ๋ฒ•์ƒ 1
1883
- ํ˜์˜๊นŒ์ง€ 1
1884
- ์ฒ˜ํ•˜๋„๋ก 1
1885
- ์šฉ์‚ฐ๊ฒฝ์ฐฐ์„œ 1
1886
- ์šด์ „ํ•˜๋‹ค 1
1887
- ํŒ๋‹จ๋˜๋ฉด 1
1888
- ๊ฐ€๋Šฅํ•˜๋‹ค๊ณ  1
1889
- ์‹๋‹น์—์„œ 1
1890
- ์ง€์†๋๊ธฐ 1
1891
- ์ ์šฉ๋˜๋ฉด 1
1892
- ๋ถ‰์–ด์ง€๊ณ  1
1893
- ๋น„ํ‹€๊ฑฐ๋ฆฌ๋ฉฐ 1
1894
- ์ฃผํ–‰ํ•˜๋‹ค 1
1895
- ์˜คํ† ๋ฐ”์ด 1
1896
- ๋ฐœ์ƒํ–ˆ๋‹ค 1
1897
- ์œ„ํ—˜์น˜์‚ฌ์ƒ 1
1898
- ๊ธฐ์†Œํ–ˆ๊ณ  1
1899
- ์„ ๊ณ ํ–ˆ๋‹ค 1
1900
- ์ œ์กฐ์‚ฌ์ธ 1
1901
- ๋ฉ”๋ฅด์„ธ๋ฐ์Šค๋ฒค์ธ  1
1902
- ์ˆ˜์ž…์‚ฌ์ธ 1
1903
- ๋ฉ”๋ฅด์„ธ๋ฐ์Šค๋ฒค์ธ ์ฝ”๋ฆฌ์•„ 1
1904
- ํ•œ์„ฑ์ž๋™์ฐจ 1
1905
- ๋ฒค์ธ ํŒŒ์ด๋‚ธ์…œ์„œ๋น„์Šค์ฝ”๋ฆฌ์•„ 1
1906
- ์„œ์šธ์ค‘์•™์ง€๋ฒ• 1
1907
- ์ œ์ถœํ–ˆ๋‹ค๊ณ  1
1908
- ๊ณต์ •๊ฑฐ๋ž˜์œ„์›ํšŒ 1
1909
- ํฌํ•จ๋˜์ง€ 1
1910
- ์†Œ์œ ์ž๋“ค 1
1911
- ์ฃผ์žฅํ•˜๊ณ  1
1912
- ํƒ‘์žฌ๋์ง€ 1
1913
- ํฌ๋ฆฌ์Šคํ† ํ”„ 1
1914
- ์Šคํƒ€์ง„์Šคํ‚ค 1
1915
- ์žฅ์ฐฉ๋œ๋‹ค 1
1916
- ์•Œ์•˜๋‹ค๋ฉด 1
1917
- ๋ถ€๋‹ดํ•ด์•ผ 1
1918
- ์†ํ•ด๋ณด๋‹ค 1
1919
- ๋ฐฐ์ƒํ•˜๊ฒŒ 1
1920
- ์†Œ์™ธ๋๋‹ค 1
1921
- ๋‹จ์ ์œผ๋กœ 1
1922
- ์— ์—ํ”„์— ์ฝ”๋ฆฌ์•„ 1
1923
- ํ˜„๋Œ€์‚ฌ๋ฃŒ 1
1924
- ํ€„๋ฆฌํƒ€์Šค๋ฐ˜๋„์ฒด 1
1925
- ํ๋ผํ‹ฐ์Šค 1
1926
- ์‚ผ์„ฑ์ฆ๊ถŒ 1
1927
- ๋ฐ˜๋„์ฒด์ฃผ 1
1928
- ์ด์–ด๊ฐ€๊ณ  1
1929
- ํ†ต์˜์ฒด์œก๊ด€ 1
1930
- ๋งž๋ถ™์—ˆ๋‹ค 1
1931
- ํ›„์œ„์—์„œ 1
1932
- ์ŠคํŒŒ์ดํฌ 1
1933
- ๋ฐ”๊นฅ์œผ๋กœ 1
1934
- ๋ฒค์น˜์—์„œ 1
1935
- ๋›ฐ์–ด์˜ฌ๋ž๋˜ 1
1936
- ๋‚˜๊ฐ”๋‹ค๊ณ  1
1937
- ์šด์šฉํ•˜๊ณ  1
1938
- ์ธ์ •ํ•˜๊ณ  1
1939
- ์„ ์ˆ˜์—๊ฒŒ 1
1940
- ์ฃผ์–ด์ง„๋‹ค 1
1941
- ๋ถˆํ•„์š”ํ•œ 1
1942
- ๊ฒฝ๊ธฐ์—์„  1
1943
- ์—ฌ์ž๋ถ€์—์„  1
1944
- ์กฐ๋ณ„๋ฆฌ๊ทธ 1
1945
- ํŽ˜ํผ์ €์ถ•์€ํ–‰ 1
1946
- ์ฃผ์ธ๊ณต์ด์—ˆ๋‹ค 1
1947
- ๋„ค์ด์…˜์Šค 1
1948
- ๋„์ž…ํ–ˆ๋‹ค 1
1949
- ์ง€๊ธ‰ํ•œ๋‹ค 1
1950
- ๋ณด์ƒ์œผ๋กœ 1
1951
- ํŽ˜์–ดํ”Œ๋ ˆ์ด 1
1952
- ์œ ๋„ํ•˜์ž 1
1953
- ํŽ˜์–ดํ”Œ๋ ˆ์ด์ƒ 1
1954
- ๋ฐ˜์˜ํ•œ๋‹ค 1
1955
- ๋ฐ˜์˜ํ•˜๊ธฐ๋กœ 1
1956
- ๊ธˆ์ „์ ์œผ๋กœ 1
1957
- ์œ ์ธ์œผ๋กœ 1
1958
- ์ž‘์šฉํ•˜์ง€ 1
1959
- ํ™๋ณดํŒ€์žฅ 1
1960
- ์ •์ฐฉํ•˜๊ณ  1
1961
- ํ™ˆ๋Ÿฐํฌ๋กœ 1
1962
- ์•ž์žฅ์„ฐ๋‹ค 1
1963
- ๊ธฐํšŒ์—์„œ 1
1964
- ๋‚ด์•ผํ”Œ๋ผ 1
1965
- ๊ฐ€๋ผ์•‰์•˜๋˜ 1
1966
- ๋ฐ›์•„์ณค๋‹ค 1
1967
- ํ™ˆ๋Ÿฐ์ด์—ˆ๋‹ค 1
1968
- ์—์ด๋“œ๋ฆฌ์–ธ 1
1969
- ๋ถ€๋Ÿฌ์กŒ์ง€ 1
1970
- ํ…Œ์˜ค์Šค์นด 1
1971
- ๋ฏธ์ณค์œผ๋‚˜ 1
1972
- ๋ถˆํŽœํˆฌ์ˆ˜ 1
1973
- ์ง‘์–ด์‚ผํ‚ฌ 1
1974
- ๋Œ๋ ค๊ฐ€๋‹ค 1
1975
- ๋ฌด์•ˆํƒ€๋กœ 1
1976
- ์นจ๋ฌตํ–ˆ๋‹ค 1
1977
- ๊ธฐ๋ถ€ํ•˜๊ธฐ๋กœ 1
1978
- ์•ฝ์ •ํ–ˆ๋‹ค๊ณ  1
1979
- ์ถœ์ƒ์œผ๋กœ 1
1980
- ์กธ์—…ํ•˜๊ณ  1
1981
- ๋ฏธ๊ตญ์œผ๋กœ 1
1982
- ์ƒ์žฅ์‹œ์ผฐ๋‹ค 1
1983
- ํ•œ๋ฏธ์ƒํ˜ธ๋ฐฉ์œ„์กฐ์•ฝ 1
1984
- ๋Œ€๊ตญ์œผ๋กœ 1
1985
- ๋งˆ๋ จํ–ˆ๊ธฐ 1
1986
- ์ฐธ์—ฌํ–ˆ๊ณ  1
1987
- ์กฐ์„ฑ๋๋‹ค๊ณ  1
1988
- ์ถœ์‹œํ–ˆ๋‹ค๊ณ  1
1989
- ์˜ค์„ธ์•„๋‹ˆ์•„ 1
1990
- ์‹ค์‹œํ–ˆ๋‹ค 1
1991
- ์•ผ์‹ฌ์ž‘์œผ๋กœ 1
1992
- ์ถœ์‹œ๋๋‹ค 1
1993
- ํฌ๋ž˜ํ”„ํ†ค 1
1994
- ์ง„ํ–‰ํ•œ๋‹ค๊ณ  1
1995
- ๊ธฐ๋ถ€๊ทธ๋ผ์šด๋“œ 1
1996
- ๋ฐฐํ‹€๊ทธ๋ผ์šด๋“œ 1
1997
- ์ฆ๊ธฐ๋ฉด์„œ 1
1998
- ๊ณต์ •๊ฑฐ๋ž˜์œ„์›ํšŒ๋กœ 1
1999
- ์„œ์šธ๊ณ ๋ฒ• 1
2000
- ๊ณต์ •๊ฑฐ๋ž˜๋ฒ• 1
2001
- ๋ถ€๊ณผํ–ˆ๋‹ค 1
2002
- ์กฐ์ž‘์œผ๋กœ 1
2003
- ๋ถ€๋‹นํ•˜๊ฒŒ 1
2004
- ์†Œ์†กํ–ˆ๊ณ  1
2005
- ํšŒ๋ณตํ•˜๊ธฐ 1
2006
- ๊ธฐ๊ฐํ–ˆ๋‹ค 1
2007
- ์„ฑ๊ฒฉ์ด๊ธฐ 1
2008
- ์ •๋‹นํ•œ์ง€ 1
2009
- ์ตœ์ข…์ ์ธ 1
2010
- ์žฌํŒ์—์„œ 1
2011
- ๋‚ด๋ ค์ง„๋‹ค 1
2012
- ์œ ๋ ฅํ•˜๋‹ค 1
2013
- ๊ธฐ์—…๊ณต๊ฐœ 1
2014
- ๋งˆ๋ฌด๋ฆฌ๋  1
2015
- ์ธ๋„์ฆ๊ถŒ๊ฑฐ๋ž˜์œ„์›ํšŒ 1
2016
- ์˜ˆ์ƒ๋Œ€๋กœ 1
2017
- ์ƒ์žฅํ•˜๋ฉด 1
2018
- ํŒ๋งคํ–ˆ๋‹ค 1
2019
- ์žฅ๋ถ€๊ฐ€์•ก 1
2020
- ์‚ญ์ œํ•˜๋ฉด์„œ 1
2021
- ์ตœ๊ณ ์ธ๋ฏผํšŒ 1
2022
- ์‹œ์ •์—ฐ์„ค 1
2023
- ์ง€์‹œํ–ˆ์—ˆ๋‹ค 1
2024
- ํŒŒ๊ธฐํ•˜๊ณ  1
2025
- ๊ฐœ์„ฑ๊ณต๋‹จ 1
2026
- ๋‚จ๋ถ๊ธฐ๋ณธํ•ฉ์˜์„œ 1
2027
- ์ž ์ •์ ์œผ๋กœ 1
2028
- ๊นŽ์•„๋‚ด๋ ธ๋Š”๋ฐ 1
2029
- ์ œ๊ธฐ๋œ๋‹ค 1
2030
- ํ•ด์ƒ๊ตญ๊ฒฝ์„  1
2031
- ์ˆ˜์—ญํ™”ํ•˜๋ ค 1
2032
- ๊ด€์ธกํ•œ๋‹ค 1
2033
- ๊ตฌ์ฒด์ ์œผ๋กœ 1
2034
- ๋ช…์‹œํ•˜๊ธฐ๋ณด๋‹ค 1
2035
- ๋ชจํ˜ธํ•˜๊ฒŒ 1
2036
- ์ข…๋ฃŒ๋๋‹ค 1
2037
- ๋…ธ๋™์‹ ๋ฌธ 1
2038
- ํ•œ๊ตญ์€ํ–‰ 1
2039
- ๋ถˆํ™•์‹ค์„ฑ 1
2040
- ์ง€์ •ํ•™์  1
2041
- ๋ฏธ์ณค๋‹ค๊ณ  1
2042
- ํ™•๋Œ€๋˜๊ณ  1
2043
- ์ค‘์žฅ๊ธฐ์ฑ„๊ถŒ 1
2044
- ๊ณ„์†๋˜๋ฉด์„œ 1
2045
- ์ˆœ์œ ์ž…๋๋‹ค 1
2046
- ์ˆœ์œ ์ถœ๋กœ 1
2047
- ์„ ๊ณ ๋ฐ›์•˜๋‹ค 1
2048
- ์ธ์ฒœ์ง€๋ฒ• 1
2049
- ์ •๋ณดํ†ต์‹ ๋ง 1
2050
- ์ด์šฉ์ด‰์ง„ 1
2051
- ์ •๋ณด๋ณดํ˜ธ 1
2052
- ์„ ๊ณ ํ–ˆ๋‹ค๊ณ  1
2053
- ์‚ฌํšŒ๋ด‰์‚ฌ 1
2054
- ๋ช…๋ นํ–ˆ๋‹ค 1
2055
- ๋‚จ์ž์นœ๊ตฌ 1
2056
- ์•Œ๋ ค์ฃผ๊ณ  1
2057
- ๋„˜๊ฒจ์กŒ๋‹ค 1
2058
- ์ œ์•ˆ์œผ๋กœ 1
2059
- ๋ชจ์ง‘ํ•˜๋ฉด 1
2060
- ๊ด€๋ฆฌํ–ˆ๋‹ค 1
2061
- ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ 1
2062
- ๊ฐœ์ธ์ •๋ณด 1
2063
- ์ž…๋ ฅํ•˜๋ฉด 1
2064
- ๋“ค๋ €๋Š”์ง€ 1
2065
- ์นœ๊ตฌ์—๊ฒŒ 1
2066
- ๊ณ ์†Œํ•˜๊ธฐ 1
2067
- ๋ด‰์ธํ–ˆ๋‹ค 1
2068
- ์—ฐ๋ฐœํ•˜๋ฉฐ 1
2069
- ์ž๋ฉธํ–ˆ๋‹ค 1
2070
- ์„ค์š•ํ•˜๋ฉด์„œ 1
2071
- ์ˆ˜๋น„์—์„œ 1
2072
- ๋‹๋ณด์˜€๋‹ค 1
2073
- ํ˜ธ์ˆ˜๋น„๋กœ 1
2074
- ๊ฐ™์•„์กŒ๋‹ค 1
2075
- ๋ผˆ์•„ํŒ ๋‹ค 1
2076
- ์ด์–ด๊ฐ€์ง€ 1
2077
- ํ™œ๋ฐœํ•˜๊ฒŒ 1
2078
- ๊ณต๊ฒฉํ–ˆ๋‹ค 1
2079
- ๋”๋ธ”์Šคํ‹ธ๋กœ 1
2080
- ์„ฑ๊ณตํ•˜๋ฉด์„œ 1
2081
- ์‹คํŒจํ•˜๋ฉด์„œ 1
2082
- ์ƒํ™ฉ์ด๋ฉด 1
2083
- ์—ญ์ „ํ–ˆ๋‹ค 1
2084
- ํฌ์ƒ๋ฒˆํŠธ 1
2085
- ๋•Œ๋ ธ๋Š”๋ฐ 1
2086
- ์„œ๋‘๋ฅด๋˜ 1
2087
- ๋น ๋œจ๋ ธ๋‹ค 1
2088
- ๋ฐŸ์œผ๋ฉด์„œ 1
2089
- ๊ธฐ์šธ์–ด์ง€ 1
2090
- ๋งˆ๋ฌด๋ฆฌํˆฌ์ˆ˜ 1
2091
- ๋‚ด์•ผ์•ˆํƒ€ 1
2092
- ๋ชฐ๋ ธ์œผ๋‚˜ 1
2093
- ๋›ฐ์–ด์˜ค๋ฅด๋ฉด์„œ 1
2094
- ๋Œ€๋“ฑํ–ˆ๋‹ค 1
2095
- ์„ฑ๊ณตํ–ˆ๊ณ  1
2096
- ํฌ์ƒํ”Œ๋ผ์ด๋กœ 1
2097
- ์ˆ˜๋น„์—์„  1
2098
- ๋ถ€์ง„ํ–ˆ๋‹ค 1
2099
- ๊ตฌ์›ํˆฌ์ˆ˜๋กœ 1
2100
- ์ธ์ƒํ•œ๋‹ค 1
2101
- ๋ถ€๋‹ดํ•˜์ง€ 1
2102
- ๋น„ํ–‰์‹œ๊ฐ„ 1
2103
- ์ธ์ƒ๋œ๋‹ค 1
2104
- ์˜ค๋ฅด๋ฉด์„œ 1
2105
- ๋Œ€ํ•œํ•ญ๊ณต 1
2106
- ์ œ์ฃผํ•ญ๊ณต 1
2107
- ๋…ธ์„ ๋ณ„๋กœ 1
2108
- ์ธ์ƒํ–ˆ๋‹ค 1
2109
- ํŒŒ๋ฆฌ์˜ฌ๋ฆผํ”ฝ 1
2110
- ๊ธˆ๋ฉ”๋‹ฌ๋ฆฌ์ŠคํŠธ 1
2111
- ๋ณต๊ท€ํ–ˆ๋‹ค 1
2112
- ์Ÿ์•„์ง€์ž 1
2113
- ๋ฐ€์–‘์—์„œ 1
2114
- ์ „๊ตญ์ฒด์œก๋Œ€ํšŒ 1
2115
- ์˜ˆ์„ ์—์„œ 1
2116
- ๋ถ€์‚ฐ๊ด‘์—ญ์‹œ 1
2117
- ์ธ์ฒœ๊ด‘์—ญ์‹œ 1
2118
- ๋ชจ์Šต์ด์—ˆ๋‹ค 1
2119
- ๋งŽ์•˜๋‹ค๊ณ  1
2120
- ์ง„์ •์‹œํ‚จ 1
2121
- ์‚ฌ๋ž‘ํ•ด์ค€ 1
2122
- ๊ฐ์‚ฌํ•˜๋‹ค 1
2123
- ๋ถˆ์ฐธํ•˜๊ณ  1
2124
- ์—ฐํ˜ธํ•˜๋ฉฐ 1
2125
- ์œ ๋„ํ•˜๋ฉฐ 1
2126
- ๋งˆ๋ฌด๋ฆฌํ–ˆ๋‹ค 1
2127
- ๊ด€์ค‘์—๊ฒŒ 1
2128
- ํ™”๋‹ตํ–ˆ๋‹ค 1
2129
- ๊ฐ์‚ฌํ–ˆ๊ณ  1
2130
- ๊ทธ๋“ค์—๊ฒŒ 1
2131
- ์กฐ๊ธˆ์ด๋ผ 1
2132
- ํ‘œํ˜„ํ•˜๊ณ  1
2133
- ๊ธฐ๋‹ค๋ ธ๋˜ 1
2134
- ๋งŒ์กฑ์Šค๋Ÿฝ๋‹ค 1
2135
- ๋ณ‘ํ–‰ํ•˜๋ฉฐ 1
2136
- ์ข‹์•„์กŒ๋‹ค 1
2137
- ๊ตญ์ œ๋Œ€ํšŒ 1
2138
- ๋ณด์—ฌ๋“œ๋ฆฌ๊ฒ ๋‹ค 1
2139
- ์ด๋ค˜์œผ๋‹ˆ 1
2140
- ๋น„ํŒํ•˜๋ฉด์„œ 1
2141
- ์ด๋ค„์ง€๊ณ  1
2142
- ์ด๋ค„์ง€๋Š”์ง€ 1
2143
- ์•Š์•˜์—ˆ๊ณ  1
2144
- ๋ฐœํ‘œํ•˜๊ณ  1
2145
- ์ถœ์ „ํ•˜์ง€ 1
2146
- ๋ณธ๋ชจ์Šต์œผ๋กœ 1
2147
- ์•Š์„๊นŒ๋ผ๊ณ  1
2148
- ์ƒ๊ฐํ•˜๊ณ  1
2149
- ๋ง๋ถ™์˜€๋‹ค 1
2150
- ๋จธ๋ฌผ๋ €๋˜ 1
2151
- ๋Šฆ๊ฒŒ๋‚˜๋งˆ 1
2152
- ์•„์‰ฌ์› ๋Š”๋ฐ 1
2153
- ์ง€์—ญ์ ์ธ 1
2154
- ๋ชปํ–ˆ๋‹ค๋ฉด 1
2155
- ์ธ์‹ํ•˜๊ฒŒ 1
2156
- ์Œ์‹๊นŒ์ง€ 1
2157
- ํ•œ๊ตญ์œผ๋กœ 1
2158
- ์ ๋ฆฌ๋ฉด์„œ 1
2159
- ์ฃผ๋ชฉํ•˜์ง€ 1
2160
- ์†Œ์„ค์ธ๋ฐ 1
2161
- ์„ฌ์„ธํ•˜๊ณ  1
2162
- ๊ฒช์–ด์™”๋˜ 1
2163
- ์ •์น˜์ ์ธ 1
2164
- ํ˜•์ƒํ™”ํ•ด์˜จ 1
2165
- ๊ทผ์›์ ์ธ 1
2166
- ์ž‘๊ฐ€๋กœ์„œ 1
2167
- ์–ต๋ˆŒ๋ ค์™”๋˜ 1
2168
- ๋ฒˆ์—ญ๊ฐ€๋“ค 1
2169
- ์ฐจ์›์—์„œ 1
2170
- ์ œ์•ˆํ–ˆ๋‹ค 1
2171
- ๋ณด๋‚ด๋А๋ƒ 1
2172
- ์ด์•ผ๊ธฐํ•ด์™”์ง€ 1
2173
- ํ•œ๋ชซํ–ˆ๋‹ค 1
2174
- ์ถ•์ ๋ผ์˜จ 1
2175
- ์–‘์„ฑํ•˜๊ณ  1
2176
- ๊ตญ์ œ๋ฌด๋Œ€ 1
2177
- ์„ธ๊ณ„๋ฌธํ•™ 1
2178
- ๋˜์–ด๊ฐ€๊ณ  1
2179
- ์žฌํ‰๊ฐ€๋˜๋ฉฐ 1
2180
- ์—ญ์‚ฌ๊นŒ์ง€ 1
2181
- ์‹ฌ์ธต์ ์œผ๋กœ 1
2182
- ๋“ค์—ฌ๋‹ค๋ณด๋ฉฐ 1
2183
- ๋‚˜๋ผ์ธ์ง€ 1
2184
- ์ธํ„ฐ๋„ท์ „๋ฌธ์€ํ–‰๋ณด๋‹ค 1
2185
- ์ทจ๊ธ‰ํ•˜๊ณ  1
2186
- ์ „์—…์ฃผ๋ถ€ 1
2187
- ๋ถ„๋ฅ˜๋œ๋‹ค 1
2188
- ํฌ์šฉํ•˜์ง€ 1
2189
- ๋ชปํ•œ๋‹ค๋ฉด 1
2190
- ๊ฑฐ๋‘ฌ๋“ค์ด๊ณ  1
2191
- ํฌ์šฉํ•˜์ž 1
2192
- ๋‚ด์„ธ์šฐ์ง€ 1
2193
- ์ธํ„ฐ๋„ท์€ํ–‰์— 1
2194
- ๋– ๋„˜๊ฒจ๋ฒ„๋ฆฐ๋‹ค 1
2195
- ์ธํ„ฐ๋„ท์ „๋ฌธ์€ํ–‰ 1
2196
- ์ž์‚ฐ์œผ๋กœ 1
2197
- ์ธํ„ฐ๋„ท์€ํ–‰๋ณด๋‹ค 1
2198
- ๊ฐ€๊ณ„๋Œ€์ถœ 1
2199
- ์ธํ„ฐ๋„ท์€ํ–‰์—์„  1
2200
- ์ธํ„ฐ๋„ท์€ํ–‰์œผ๋กœ 1
2201
- ๋‹ฌ๋ ค๊ฐ€๊ณ  1
2202
- ํŒํŒํ•ด์ง„ 1
2203
- ์ค‘์ €์‹ ์šฉ์ž๋“ค 1
2204
- ์ค‘์ €์‹ ์šฉ๋Œ€์ถœ 1
2205
- ์ทจ์•ฝ๊ณ„์ธต 1
2206
- ๋ฐ€๋ ค๋‚˜๊ณ  1
2207
- ๋ฐฉ์ฆ์ด๋‹ค 1
2208
- ๋„˜์–ด์„œ๊ธฐ 1
2209
- ๊ธˆ์œต๋‹น๊ตญ 1
2210
- ๊ด€๋ฆฌํ•˜๊ณ  1
2211
- ์ธํ„ฐ๋„ท์€ํ–‰๋งŒ 1
2212
- ์ด์ž์ด์ต 1
2213
- ๋‹น๊ธฐ์ˆœ์ด์ต 1
2214
- ๊ณ ๋ฏผํ•ด์•ผ 1
2215
- ๋งˆ๋ฌด๋ฆฌํ•  1
2216
- ์‚ฐ์—…ํ†ต์ƒ์ž์›๋ถ€ 1
2217
- ๊ฐœ๋ฐœ๊ถŒ์ธ 1
2218
- ๋ถ€์—ฌํ•˜๊ณ  1
2219
- ํŠน๋ณ„์ˆ˜๋‹น 1
2220
- ํ•ด์ €๊ด‘๋ฌผ์ž์› 1
2221
- ์ž…๋ฒ•์˜ˆ๊ณ ํ•œ๋‹ค๊ณ  1
2222
- ๊ฐœ์ •ํ•˜๊ธฐ๋กœ 1
2223
- ์ƒ์‚ฐํ•˜๋ฉด 1
2224
- ์กฐ๊ด‘๋ฃŒ๋กœ 1
2225
- ๊ฑฐ๋‘๋„๋ก 1
2226
- ์ œ๋„์—์„œ 1
2227
- ๋Š˜์–ด๋‚ ์ˆ˜๋ก 1
2228
- ๊ธ‰์ฆํ•˜๋ฉด 1
2229
- ์‹ ์„ค๋œ๋‹ค 1
2230
- ๊ณต์‹์ ์œผ๋กœ 1
2231
- ๊ด‘๊ตฌ์—์„œ 1
2232
- ๋ฐœ๊ฒฌํ•˜๋ฉด 1
2233
- ๊ณ„์•ฝ์—์„œ 1
2234
- ๋„˜์–ด์„œ๋ฉด 1
2235
- ๊ด€๋ฆฌ์ž๋กœ 1
2236
- ๋ณต๊ท€ํ•œ๋‹ค 1
2237
- ๊ด€์žฅํ•˜๊ณ  1
2238
- ์ „๋žต์ ์ธ 1
2239
- ์ž‰๊ธ€๋žœ๋“œ 1
2240
- ๊ฐ๋…์œผ๋กœ 1
2241
- ๋ฌผ๋Ÿฌ๋‚˜์ž 1
2242
- ๊ด€์žฅํ•˜๋ฉฐ 1
2243
- ๋ฐ”์ด์—๋ฅธ 1
2244
- ์†Œ์œ ํ•˜๊ณ  1
2245
- ๋ฐ”๋€Œ๊ฒ ์ง€ 1
2246
- ์œ ๋ง์ฃผ๋“ค 1
2247
- ๋ฐœ์ „์‹œํ‚ค๊ณ  1
2248
- ๋ฐ˜๋ฐœํ•˜๊ณ  1
2249
- ์Ÿ์•„๋ถ€์–ด 1
2250
- ํŒฌ๋“ค์—๊ฒ 1
2251
- ๋ถ„๋ฐ์Šค๋ฆฌ๊ฐ€์—์„  1
2252
- ๋ง์ด๋ผ๊ณ  1
2253
- ๊ฐ•์กฐํ•˜๋ฉฐ 1
2254
- ๋ชธ๋‹ด์•˜๋˜ 1
2255
- ๋„๋ฅดํŠธ๋ฌธํŠธ 1
2256
- ๋น„์•„๋ƒฅ๊ฑฐ๋ฆฌ๊ณ  1
2257
- ๋Œ์–ด๋‚ด๋ ค์•ผ 1
2258
- ๊ฐ•ํ™”๊ตฐ์ˆ˜ 1
2259
- ์‹ฌ๊ฐํ•˜๋‹ค๋ฉด 1
2260
- ๋„์ค‘์—๋ผ 1
2261
- ๋Œ์–ด๋‚ด๋ฆฌ 1
2262
- ๋ฏผ์ฃผ์ฃผ์˜๊ณ  1
2263
- ๋ฐœ์–ธ์ด์—ˆ์ง€ 1
2264
- ๋งŒ๋“ค๊ฒ ๋‹ค๊ณ  1
2265
- ๊ถŒ์ขŒ์—์„œ 1
2266
- ๊ตฌ์„ฑํ•˜๊ณ  1
2267
- ์˜์›ํšŒ๊ด€ 1
2268
- ์ •๋ฉด์œผ๋กœ 1
2269
- ๋ฐ˜ํ—Œ๋ฒ•์  1
2270
- ๋ฐ˜๋ฐœํ–ˆ๋‹ค 1
2271
- ์•„๋‹ˆ์—ˆ๋‹ค๊ณ  1
2272
- ํ”ผ๊ฐ๊ธฐ๊ด€ 1
2273
- ์˜ˆ๊ณ ํ•˜๊ณ  1
2274
- ํ‚ค์ฆˆ์นดํŽ˜ 1
2275
- ์ƒ๊ฐ๋‚˜์„œ 1
2276
- ์ฐพ์•„๋ณด๋‹ˆ 1
2277
- ์”์“ธํ–ˆ๋‹ค 1
2278
- ์–ด๋ฅด์‹ ๋“ค 1
2279
- ๊ฒฝ๋กœ๋‹น์—” 1
2280
- ์žฅ๊ธฐ์š”์–‘์ธ์ •๋“ฑ๊ธ‰ 1
2281
- ์–ด๋ฆฐ์ด์ง‘ 1
2282
- ๊ฐ์†Œํ–ˆ๋‹ค 1
2283
- ์„œ์šธ์‹œ๊ต์œก์ฒญ 1
2284
- ์‹ค๋ฒ„ํƒ€์šด 1
2285
- ์‹ค๋ฒ„ํƒ€์šด์œผ๋กœ 1
2286
- ์…”ํ‹€๋ฒ„์Šค๋กœ 1
2287
- ๋ฐ๋ ค์˜ค๊ณ  1
2288
- ๋ฐ๋ ค๋‹ค์ค€๋‹ค 1
2289
- ๋น„์Šทํ•˜๋‹ค 1
2290
- ์œ ์น˜์›์žฅ 1
2291
- ๋…ธ์น˜์›์œผ๋กœ 1
2292
- ํŽธ์ด๋ผ๊ณ  1
2293
- ์‹œ๋„ํ•˜๊ณ  1
2294
- ํ•™์Šต์ง€๋กœ 1
2295
- ๋ฐฐ๋‹ฌํ•˜๊ณ  1
2296
- ๋‹จ์ง€์—์„œ 1
2297
- ๊ธฐ๋ถ€์ฑ„๋‚ฉ 1
2298
- ์ œ์‹œํ•˜์ž 1
2299
- ์†Œ์œ ์ฃผ๋“ค 1
2300
- ์ ‘์–ด๋“ค์–ด 1
2301
- ๋ถ„๋ช…ํ•˜๋‹ค 1
2302
- ์„ ์ง„๊ตญ์—์„  1
2303
- ์‹œ์„ค์ด๊ธฐ 1
2304
- ๋Š˜๋ ค๊ฐ€์•ผ 1
2305
- ์ตœ๊ทผ๋ถ€ํ„ฐ 1
2306
- ๋‚ด๋ ค๊ฐ€๋ฉด์„œ 1
2307
- ์‹ฌํ•ด์ง€๋ฉด 1
2308
- ๋ฉœ๋ผํ† ๋‹Œ 1
2309
- ๋นŒ๋ ค๋ณด๊ณ  1
2310
- ์‚ฌ์šฉํ•ด๋ดค๋‹ค 1
2311
- ์˜๋ฃŒ๊ธฐ๊ธฐ 1
2312
- ์‚ผ์„ฑ๋ฉ”๋””์Šจ 1
2313
- ์—ฐ๊ตฌ์†Œ์žฅ 1
2314
- ์ œํ’ˆ์ด๋‹ค 1
2315
- ์Šฌ๋ฆฌํ”ผ์†” 1
2316
- ๊ฑฐ์Šฌ๋ฆฌ์ง€ 1
2317
- ์ด๊ณณ์œผ๋กœ 1
2318
- ์ „๋‹ฌํ•œ๋‹ค 1
2319
- ์ŠคํŠธ๋ ˆ์Šค 1
2320
- ๋น„์•ฝ๋ฌผ์  1
2321
- ์น˜๋ฃŒ๋ฒ•์ด๋ผ๊ณ  1
2322
- ์„œ์šธ๋Œ€ํ•™๊ต 1
2323
- ๋ถ„๋‹น๋ณ‘์› 1
2324
- ์‚ฌ์šฉํ–ˆ๋”๋‹ˆ 1
2325
- ํ˜ธ์ „๋๋‹ค๊ณ  1
2326
- ๋‹ตํ–ˆ๋‹ค๊ณ  1
2327
- ์„ค์น˜ํ•˜๊ณ  1
2328
- ๋ธ”๋ฃจํˆฌ์Šค๋กœ 1
2329
- ๊ธฐ๋ณธ์ด๋‹ค 1
2330
- ์Šค๋งˆํŠธ๊ธฐ๊ธฐ 1
2331
- ๋ถ„์„ํ•ด์ฃผ๊ณ  1
2332
- ์ ์‘ํ•˜๊ธฐ 1
2333
- ์–ด๋ ค์› ์œผ๋‚˜ 1
2334
- ์‹œํ‹ฐํ•„๋“œ 1
2335
- ์ง€๋ชฉ๋๋˜ 1
2336
- ํ”„๋ž€์‹œ์Šค์ฝ” 1
2337
- ํˆฌ์ž…ํ•˜์ž 1
2338
- ์ง์„ ์œผ๋กœ 1
2339
- ๋„˜๊ฒจ๋ฒ„๋ ธ๋‹ค 1
2340
- ์˜ฌ๋ผ๊ฐ€์•ผ 1
2341
- ๊ฐ๊ฒฉํ–ˆ๋‹ค 1
2342
- ๋›ฐ์–ด๋“ค์—ˆ๋‹ค 1
2343
- ๋ณผํ‹ฐ๋ชจ์–ด 1
2344
- ํŒ€์ด์—ˆ๋‹ค 1
2345
- ๋ง‰ํŒ๊นŒ์ง€ 1
2346
- ๊ธฐ์‚ฌํšŒ์ƒํ•œ 1
2347
- ๋ชจ์–‘์ƒˆ๋‹ค 1
2348
- ๊ทน์ ์œผ๋กœ 1
2349
- ํ™•์ •ํ•˜๋”๋‹ˆ 1
2350
- ์›์ •๊ฒฝ๊ธฐ๋กœ 1
2351
- ๊ฐ€๋ ค์ง„๋‹ค 1
2352
- ๋‹ฌ์•„๋‚ฌ๋‹ค 1
2353
- ํ™ˆ๋Ÿฐ์œผ๋กœ 1
2354
- ์œ ๋ฆฐํ–ˆ๋‹ค 1
2355
- ์˜ˆ๊ณ ๋๋‹ค 1
2356
- ์™ธ๋‚˜๋ฌด๋‹ค๋ฆฌ 1
2357
- ์Šน๋ถ€์—์„œ 1
2358
- ๋“ฑํŒํ•œ๋‹ค 1
2359
- ๊ฐ€๊นŒ์Šค๋กœ 1
2360
- ๊ฐ€๋ผ์•‰์•„ 1
2361
- ๊ณ ๋ฏผ์ด๋‹ค 1
2362
- ์‹ ์•ฝ๊ฐœ๋ฐœ์—…์ฒด์ธ 1
2363
- ์•Œํ…Œ์˜ค์  ์ธ 1
2364
- ๊ฑฐ๋ž˜๋˜๋˜ 1
2365
- ๋ฉด์—ญํ•ญ์•”์ œ 1
2366
- ์ ์šฉ๋๋‹ค 1
2367
- ์ž‘์šฉํ–ˆ๋‹ค 1
2368
- ๊ฒฌ์ œํ•˜๊ธฐ 1
2369
- ํ†ต๊ณผ๋˜๋ฉด 1
2370
- ๋ฉ”๋ฆฌ์ธ ์ฆ๊ถŒ 1
2371
- ์•„๋ชจ๋ ˆํผ์‹œํ”ฝ 1
2372
- ํ•œํ™”์—์–ด๋กœ์ŠคํŽ˜์ด์Šค 1
2373
- ์‹ ํ•œ์ง€์ฃผ 1
2374
- ์‚ผ์„ฑ์ƒ๋ช… 1
2375
- ๋Œ€์žฅ์ฃผ์ธ 1
2376
- ์ˆœ๋งค๋„ํ•˜๋ฉด์„œ 1
2377
- ์ˆœ๋งค๋„๋กœ 1
2378
- ํ•˜๋ฝํ•˜๋ฉด์„œ 1
2379
- ๊ตญ์ œํšŒ์˜ 1
2380
- ์—ด์—ˆ์–ด์š” 1
2381
- ํšŒ์˜์—์„  1
2382
- ๊ธฐํ›„๋ณ€ํ™”๋กœ 1
2383
- ์•…๋งˆํ™”๋˜๊ณ  1
2384
- ํฌ์œ ๋™๋ฌผ 1
2385
- ์ข…๋ฅ˜๋ฟ์ด๊ณ  1
2386
- ๋ถ€ํ’€๋ ค์กŒ๋‹ค 1
2387
- ์•„๋‹ดํ•ด์š” 1
2388
- ์•„๋ฅดํ—จํ‹ฐ๋‚˜ 1
2389
- ๋ถ„ํฌํ•˜์ฃ  1
2390
- ์ค‘๋‚จ๋ฏธ๋กœ 1
2391
- ๋ฐœ๊ฒฌํ•˜๊ณ  1
2392
- ๋– ์˜ฌ๋ ค์„œ 1
2393
- ํŽ„๋Ÿญ์ด๋ฉฐ 1
2394
- ๋‚ด๋ ค์•‰์•„ 1
2395
- ๋ชจ์Šต์ด์š” 1
2396
- ์•ผํ–‰์„ฑ์ด๊ธด 1
2397
- ๋‹ค๋ฅด๋‹ต๋‹ˆ๋‹ค 1
2398
- ์ €๊ณต๋น„ํ–‰ 1
2399
- ์ ํ”„ํ•˜๊ฑฐ๋‚˜ 1
2400
- ์˜ฌ๋ผ๊ฐ€์š” 1
2401
- ๋ฐ•์ฅ๋ณด๋‹ค 1
2402
- ๋Šฅ์ˆ™ํ•˜๊ฒŒ 1
2403
- ํ˜๋Ÿฌ๋‚˜์˜ค 1
2404
- ํ˜“๋ฐ”๋‹ฅ์œผ๋กœ 1
2405
- ์˜ค๋ชฉํ•˜๊ฒŒ 1
2406
- ๋“ค์ด๋งˆ์‹œ๊ธฐ 1
2407
- ๊ตฌ์กฐ์˜ˆ์š” 1
2408
- ๋ฌด๊ฒŒ๊นŒ์ง€ 1
2409
- ์žˆ๋‹ต๋‹ˆ๋‹ค 1
2410
- ๋นต๋นตํ•ด์ง„ 1
2411
- ๋„๋ธŒ๋Ÿฌ์ ธ 1
2412
- ๋ฐœ๊ฒฌ๋˜์ฃ  1
2413
- ๋งŒ๋“ค์–ด๋‚ด 1
2414
- ์ž๊ธฐ์—๊ฒŒ 1
2415
- ์ผ์–ด๋‚ฌ๋Š”์ง€ 1
2416
- ์ž”๋‹ต๋‹ˆ๋‹ค 1
2417
- ๋ฌผ๋ ธ๋‹ค๊ณ  1
2418
- ์ด๋ฆฌ์ €๋ฆฌ 1
2419
- ๋‹ค๋‹ˆ๋ฉด์„œ 1
2420
- ๋งŒ๋“ค์–ด์š” 1
2421
- ๊ตฌ์„ฑ์›๋“ค๋ผ๋ฆฌ 1
2422
- ๊ฒŒ์›Œ์ฃผ๊ธฐ 1
2423
- ์žˆ๊ฑฐ๋“ ์š” 1
2424
- ๋จน์ธ๋‹ต๋‹ˆ๋‹ค 1
2425
- ์˜ํ† ์—์„œ 1
2426
- ๊ฐ€์™€๋ฐ”ํƒ€ 1
2427
- ๋ฐฐ์ถœํ–ˆ๊ณ  1
2428
- ๋ฐฐ์ถœํ–ˆ๋‹ค 1
2429
- ๋“ฑ์žฅํ–ˆ์ง€ 1
2430
- ๊ตญ์ œ์‚ฌํšŒ์—์„œ 1
2431
- ์ค‘๋ก ์ด์—ˆ๋‹ค 1
2432
- ์ˆ˜์ƒํ•˜๋ฉด์„œ 1
2433
- ์„ฑ์žฅํ•˜๊ณ  1
2434
- ๊ธ€๋กœ๋ฒŒํ•˜๊ฒŒ 1
2435
- ๊ณต๋™์ˆ˜์ƒํ•œ 1
2436
- ๋ฌธํ•™์€์ปค๋…• 1
2437
- ์„ ์ง„๊ตญ์ธ 1
2438
- ํ†ตํ™”์—์„œ 1
2439
- ํ™•์‹คํ•˜๋‹ค 1
2440
- ํŽ˜์ธํŠธ์น  1
2441
- ํƒ๋ฏธ์ฃผ์˜์ž 1
2442
- ํšจ๊ณผ์ ์œผ๋กœ 1
2443
- ์ดํƒˆ๋ฆฌ์•„ 1
2444
- ๋ง๋ผํŒŒ๋ฅดํ…Œ 1
2445
- ํ•œ๊ตญ๋ฌธํ•™ 1
2446
- ์ ‘๊ทผํ•˜๊ณ  1
2447
- ์ž‘๋™ํ•˜๊ณ  1
2448
- ๊ฐ„๊ณผ๋ผ์„œ 1
2449
- ์ง„์ถœํ•œ๋‹ค 1
2450
- ์ด์—๋”ฐ๋ผ 1
2451
- ๋Š˜์–ด๋‚˜๊ฒŒ 1
2452
- ๋งบ์—ˆ๋‹ค๊ณ  1
2453
- ๋ฒ ์ด์ปค๋ฆฌ 1
2454
- ๊ฑฐ์ ์œผ๋กœ 1
2455
- ์ฆ๊ฐ€ํ•˜๊ณ  1
2456
- ๊ณ„์•ฝ์œผ๋กœ 1
2457
- ๋™๋‚จ์•„์‹œ์•„์—์„œ 1
2458
- ์ง„์ถœํ•˜๊ฒŒ 1
2459
- ๋ฐœ์–ธ์œผ๋กœ 1
2460
- ์ผ์œผํ‚จ๋‹ค 1
2461
- ์ง‘์ค‘์‹œํ‚ค๊ธฐ 1
2462
- ์†ก๋„์—์„œ 1
2463
- ์‹œ์žฅ์ด๋‹ค 1
2464
- ํŒŒ์‚ฐ์ด๋‹ค 1
2465
- ์‹ค๋ฒ„๊ฒŒ์ดํŠธ์€ํ–‰ 1
2466
- ์œ„๊ธฐ์—์„œ 1
2467
- ์ธ์ˆ˜๋˜๋ฉฐ 1
2468
- ํฌ๋ ˆ๋””ํŠธ์Šค์œ„์Šค 1
2469
- ์œ„ํ—˜ํ•˜๊ธฐ 1
2470
- ์ƒ๊ฐํ•œ๋‹ค 1
2471
- ๊ณ ๋ฏผํ•˜์ž 1
2472
- ํ•˜๋ฝํ•œ๋‹ค 1
2473
- ์ง€๋‚ ์ˆ˜๋ก 1
2474
- ์˜ฌ๋ผ๊ฐ„๋‹ค 1
2475
- ๋ฉ”ํŠธ๊ฐˆํ”„ 1
2476
- ๋ฒ•์น™์ด๋ž€ 1
2477
- ๋ฉ”ํŠธ์นผํ”„ 1
2478
- ์ง์„ ์ ์œผ๋กœ 1
2479
- ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ 1
2480
- ๋ฒ•์น™์ด๋‹ค 1
2481
- ๊ฐ€์†ํ™”๋ ์ˆ˜๋ก 1
2482
- ํ™”ํ๋ณด๋‹ค 1
2483
- ์•”ํ˜ธํ™”ํ 1
2484
- ํŠธ๋Ÿผํ”„๋‚˜ 1
2485
- ์Šคํƒ ํผ๋“œ๋Œ€ 1
2486
- ๋…ธ์Šค์›จ์Šคํ„ด 1
2487
- ์•ˆ์ •์ ์ธ 1
2488
- ์œคํƒํ•ด์ง„๋‹ค๊ณ  1
2489
- ์‹œ์ž‘ํ•˜๋ฉด์„œ 1
2490
- ์ผ๋ฐ˜์ ์ธ 1
2491
- ๋ด‰๊ธ‰์œผ๋กœ 1
2492
- ์•„๋ฒ„์ง€์ธ 1
2493
- ์›”๊ธ‰์œผ๋กœ 1
2494
- ํˆฌ์žํ•˜๊ธฐ 1
2495
- ํ‚ค์›Œ๋‚˜๊ฐ„ 1
2496
- ๋ถ•๊ดด์˜€๋‹ค 1
2497
- ์ด์ „๋ณด๋‹ค 1
2498
- ๋‘๋ ค์›Œํ•  1
2499
- ๋‚˜ํƒ€๋‚œ๋‹ค 1
2500
- ๋ถ€์กฑํ•˜๋‹ค 1
2501
- ๋ ˆ์ด์Šค๋ž€ 1
2502
- ๋ฒ—์–ด๋‚˜๋ ค๋ฉด 1
2503
- ์‚ฌํšŒ์—์„œ 1
2504
- ๊ธฐ๋‹ค๋ฆฌ๋˜ 1
2505
- ํŽ˜์–ด์›จ์ด 1
2506
- ์ž‘์„ฑํ–ˆ๋‹ค 1
2507
- ๋ธ”๋ฃจํ—ค๋Ÿฐ 1
2508
- ์žก์•„๋‚ด๋ฉฐ 1
2509
- ํ—ˆ์šฉํ–ˆ์œผ๋‚˜ 1
2510
- ํ•œ๊ตญ์—ฌ์ž์˜คํ”ˆ 1
2511
- ๋ฌธํ„ฑ๊นŒ์ง€ 1
2512
- ๋ชจ๋ฅด๊ฒ ๋‹ค 1
2513
- ์šฐ์Šนํ•˜๊ณ ์ž 1
2514
- ์ž„ํ•˜๋Š”๋ฐ 1
2515
- ์‹ถ์—ˆ๋Š”๋ฐ 1
2516
- ์ง€๋‚˜๊ฐ€๊ธธ 1
2517
- ์žฅํƒ€์ž์ด๋ฉด์„œ 1
2518
- ์•„์ด์–ธ์ƒท 1
2519
- ๋นผ์–ด๋‚˜๋‹ค 1
2520
- ์ข‹์•„ํ•˜์ง€ 1
2521
- ์–ด๋ ค์›Œ์„œ 1
2522
- ์กฐ๊ฑด์ด๋‹ˆ 1
2523
- ์žˆ์—ˆ๋‹ค๊ณ  1
2524
- ์ดˆ๋ฐ˜๋ถ€ํ„ฐ 1
2525
- ํ›ˆ๋ จํ•ด์˜ค๋ฉด์„œ 1
2526
- ๋…ธ๋ ฅํ–ˆ๋‹ค 1
2527
- ํ”๋“ค๋ฆฌ์ง€ 1
2528
- ๋‹จ๋‹จํ•˜๊ฒŒ 1
2529
- ์˜ฌ๋ผ์„ฐ๋‹ค 1
2530
- ์ œ์ฃผ์‚ผ๋‹ค์ˆ˜ 1
2531
- ๋งˆ์Šคํ„ฐ์Šค 1
2532
- ๋ฒˆ๋ฟ์ด์ง€ 1
2533
- ์ƒ๊ธˆ์—์„œ 1
2534
- ๋ชจ์Šต์ด๋‹ค 1
2535
- ์ž๋ฆฌํ–ˆ๋‹ค 1
2536
- ๋”ฐ๋ผ์™”๋‹ค 1
2537
- ํ•ฉ์ž‘ํ–ˆ๊ณ  1
2538
- ์•ผ๊ตฌ๋ณด๋‹ค 1
2539
- ์ƒ๊ฐํ–ˆ๋Š”๋ฐ 1
2540
- ๋“ค์–ด๋งž์œผ๋ฉด์„œ 1
2541
- ํ†ต๊ณผํ•˜๊ณ  1
2542
- ์žฅ์†Œ์—์„œ 1
2543
- ์ฟ ์—๋ฐ”์Šค 1
2544
- ์„ ๋ฐœํˆฌ์ˆ˜๋กœ 1
2545
- ์Šฌ๋ผ์ด๋” 1
2546
- ์‹ ๊ณ ํ–ˆ๋‹ค 1
2547
- ๋ถ€์‘ํ–ˆ๋‹ค 1
2548
- ์‹œ๋„ํ–ˆ๋‹ค 1
2549
- ๊ธฐ๋Œ€ํ–ˆ๋˜ 1
2550
- ์ •๊ทœ๋ฆฌ๊ทธ์—์„  1
2551
- ์ด๋‚ ๊นŒ์ง€ 1
2552
- ๋ถ€์ง„ํ•˜๋‹ค 1
2553
- ์•„์›ƒ์นด์šดํŠธ 1
2554
- ๋ฌผ๋Ÿฌ๋‚˜๊ธฐ 1
2555
- ๋ชจ๋‘์—๊ฒŒ 1
2556
- ์ ˆ์‹คํ–ˆ๋‹ค 1
2557
- ๋”˜์ด์—ˆ๋‹ค 1
2558
- ๋Œ€๊ฒฐ์—์„œ 1
2559
- ํ™˜ํ˜ธํ–ˆ๋‹ค 1
2560
- ์žฅ๊ฑฐ๋ฆฌํฌ 1
2561
- ๋“ฑํŒํ–ˆ๋‹ค 1
2562
- ์ฟ ์—๋ฐ”์Šค๋ณด๋‹ค 1
2563
- ์ดˆ๋ฐ˜๊นŒ์ง€ 1
2564
- ํ”Œ๋ผ์ด๋กœ 1
2565
- ์ถ”๊ฒฉํ–ˆ๋‹ค 1
2566
- ๊ณต๋žตํ•˜์ง€ 1
2567
- ๋„์ „ํ•˜๊ฒŒ 1
2568
- ์žฌ๋ฌด์ œํ‘œ 1
2569
- ์ ๊ทน์ ์ด์ง€ 1
2570
- ๊ธฐ์—…๋ถ„์„์—ฐ๊ตฌ์†Œ 1
2571
- ๋ฆฌ๋”์Šค์ธ๋ฑ์Šค 1
2572
- ์ถ”์ง„ํ•˜๋ฉด์„œ 1
2573
- ๋…๋ คํ•˜๊ณ  1
2574
- ํ•œ์ •๋๋‹ค 1
2575
- ๊ธฐ์—…๋ณ„๋กœ 1
2576
- ๋ฐฐ๋‹นํ–ˆ๋‹ค 1
2577
- ์žฅ๊ตฐ๋ฉ๊ตฐ 1
2578
- ์ผ์œผ์ผฐ๋˜ 1
2579
- ์–‘ํ‚ค์Šคํƒ€๋””์›€ 1
2580
- ์›์ •๊ฒฝ๊ธฐ 1
2581
- ๋˜๊ฐš์•˜๋‹ค 1
2582
- ์„ ๋‘ํƒ€์ž๋กœ 1
2583
- ์‚ด๋ฐ”๋„๋ฅด 1
2584
- ํ„ฐ๋œจ๋ ธ๊ณ  1
2585
- ์„ฑ๊ณตํ•˜์ž 1
2586
- ์ฒ˜๋ฆฌํ•˜๋ฉฐ 1
2587
- ํƒ€์„์—์„  1
2588
- ์ข…ํšก๋ฌด์ง„ํ–ˆ๋‹ค 1
2589
- ์นด์šฐํ”„๋งŒํ•„๋“œ 1
2590
- ์ž์ด์–ธ์ธ  1
2591
- ์›์ •์œผ๋กœ 1
2592
- ๋ณผํ‹ฐ๋ชจ์–ด๋กœ 1
2593
- ๋‰ด์š•์œผ๋กœ 1
2594
- ์ œ์••ํ•˜๋ฉฐ 1
2595
- ์—๋งˆ๋ˆ„์—˜ 1
2596
- ์นจ๋ฌต์‹œ์ผฐ๋‹ค 1
2597
- ๊ธฐ๋กํ•˜๋ฉด์„œ 1
2598
- ๋งˆ๋ฌด๋ฆฌ๋กœ 1
2599
- ์ด์–ด์ง€๋ฉด 1
2600
- ๋‚ด๋†“์œผ๋ฉด์„œ 1
2601
- ์„ธ๊ณ„์ ์œผ๋กœ 1
2602
- ๋ฌธ๋ฒ•๊นŒ์ง€ 1
2603
- ๋‚ด๋ฒ„๋ ค๋‘ 1
2604
- ๊ทœ์ •์ด๋ผ 1
2605
- ๊ณต์˜๋ฐฉ์†ก 1
2606
- ๋…์ผ์–ด๋กœ 1
2607
- ํ‘œ๊ธฐํ•œ๋‹ค 1
2608
- ๊ณ ์œ ๋ช…์‚ฌ 1
2609
- ํ—ˆ์šฉํ•˜๊ฒ ๋‹ค 1
2610
- ๊ณต์‹์ ์ธ 1
2611
- ์œ„์›ํšŒ์ธ 1
2612
- ๊ณต์šฉ์–ธ์–ด๋กœ 1
2613
- ๋…์ผ์ธ๋“ค 1
2614
- ์ต์ˆ™ํ•˜๊ฒŒ 1
2615
- ๋‚ด๋ ธ๋‹ค๊ณ  1
2616
- ์ „ํ†ต์ด์—ˆ์ง€ 1
2617
- ๊ฐ„ํŒ์—์„œ 1
2618
- ๋น„ํŒํ•˜๊ณ  1
2619
- ๋‚ด๋ฒ„๋ ค๋‘๊ณ  1
2620
- ํ‰๋ก ์—์„œ 1
2621
- ๋จธ๋ฆฌ์นด๋ฝ 1
2622
- ์‚ฌ๋žŒ์ด๋ผ๋ฉด 1
2623
- ํ”„๋ž‘ํ”„ํ‘ธ๋ฅดํŠธ 1
2624
- ์ผ๊ฐ„์ง€์ธ 1
2625
- ์ค‘๊ฐ„์ค‘๊ฐ„ 1
2626
- ์–ด์ƒ‰ํ•˜๊ฒŒ 1
2627
- ๋ผ์›Œ๋„ฃ์œผ๋ฉฐ 1
2628
- ๋ถˆ๊ฐ€ํ”ผํ•˜๋‹ค๊ณ  1
2629
- ๋ฒ ๋ฅผ๋ฆฐ์ž์œ ๋Œ€ํ•™ 1
2630
- ์–ธ์–ดํ•™์ž 1
2631
- ์Šคํ…ŒํŒŒ๋…ธ๋น„์น˜ 1
2632
- ํ”„๋ž‘์Šค์–ด 1
2633
- ๋ถ€์ž„ํ–ˆ๋‹ค 1
2634
- ๋–จ์–ด์กŒ๋˜ 1
2635
- ๋“ค๋ ค์คฌ๋‹ค 1
2636
- ๋“ฑ์žฅํ–ˆ๊ณ  1
2637
- ์บ๋Ÿด์ด๋ผ 1
2638
- ์ถœ๊ฐ„ํ•œ๋‹ค 1
2639
- ์…ฐ์ต์Šคํ”ผ์–ด 1
2640
- ๋‹ค์Œ์œผ๋กœ 1
2641
- ํƒ„์ƒ์ด์—ˆ๋‹ค 1
2642
- ๋“ฑ์žฅ์‹œํ‚จ๋‹ค 1
2643
- ์‚ฝํ™”์—์„œ 1
2644
- ํ‘œํ˜„๋๋‹ค 1
2645
- ๋ฒ ์ŠคํŠธ์…€๋Ÿฌ 1
2646
- ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ 1
2647
- ๋งŒ๋“ค์–ด์ง€๋ฉด์„œ 1
2648
- ์œ ๋ช…ํ•ด์กŒ์ง€ 1
2649
- ๋ฉธ์ข…ํ–ˆ๊ธฐ 1
2650
- ๋ชจ๋ฆฌ์…”์Šค์— 1
2651
- ์„œ์‹ํ–ˆ๋‹ค 1
2652
- ๋ชจ๋ฆฌ์…”์Šค 1
2653
- ๋„์ฐฉํ•˜๋ฉด์„œ 1
2654
- ์ง€๊ตฌ์—์„œ 1
2655
- ์‚ฌ๋ผ์กŒ๋‹ค 1
2656
- ๋ฉธ์ข…์‹œํ‚ค๊ณ  1
2657
- ๋’ค๋šฑ๊ฑฐ๋ฆฌ๋ฉด์„œ 1
2658
- ๋ชปํ–ˆ์œผ๋ฉฐ 1
2659
- ๋ฉ์ฒญํ•˜๊ณ  1
2660
- ๊ธฐ๋ก์œผ๋กœ 1
2661
- ํฌ๋ฅดํˆฌ๊ฐˆ์–ด๋กœ 1
2662
- ์˜๋ฏธ์—์„œ 1
2663
- ์œ ๋ž˜ํ–ˆ๋‹ค 1
2664
- ๊ตญ์ œํ•™์ˆ ์ง€ 1
2665
- ์žˆ์—ˆ์œผ๋ฉฐ 1
2666
- ์‚ดํŽด๋ณด์ž 1
2667
- ๋ฒŒ์ฑ„ํ•˜๋ฉด์„œ 1
2668
- ๋ฉ์ฒญํ•ด์„œ 1
2669
- ์›”๋“œ์ฒ˜๋Ÿผ 1
2670
- ์†Œ๋ง์ด์—ˆ๋‹ค 1
2671
- ์‚ฐํƒ€ํฌ๋ฃจ์ฆˆ 1
2672
- ๊ต์ˆ˜์ด์ž 1
2673
- ์ƒ๋ช…๊ณตํ•™ 1
2674
- ๋ฐ”์ด์˜ค์‚ฌ์ด์–ธ์Šค 1
2675
- ์ตœ๊ณ ๊ณผํ•™์ฑ…์ž„์ž 1
2676
- ์ค„๊ธฐ์„ธํฌ 1
2677
- ์ฝ”ํŽœํ•˜๊ฒ 1
2678
- ํ‘œ๋ณธ์—์„œ 1
2679
- ์–ป์—ˆ๋‹ค๊ณ  1
2680
- ์—‡๊ฐˆ๋ฆฐ๋‹ค 1
2681
- ๊ณตํ†ต์กฐ์ƒ 1
2682
- ์ง„ํ™”ํ•˜๊ธฐ 1
2683
- ๋„๋„์ธ์ง€ 1
2684
- ์ž์‹ ํ•œ๋‹ค 1
2685
- ํƒœ์ฆˆ๋ฉ”์ด๋‹ˆ์•„ 1
2686
- ํฐ์ฝ”๋ฟ”์†Œ 1
2687
- ์˜ํ™”๊ฐ๋… 1
2688
- ํˆฌ์žํ–ˆ๋‹ค 1
2689
- ์œ ๋ช…์ธ๋“ค 1
2690
- ๊ตฌ์„ฑ์›์œผ๋กœ 1
2691
- ๊ตด์ข…์ ์ธ 1
2692
- ์šธ์‚ฐ์‹œ์žฅ 1
2693
- ๋ถˆ๊ตฌํ•˜๊ณ  1
2694
- ๊ผฝ์œผ๋ผ๋ฉด 1
2695
- ๋ฌธ์ œ์ด์ง€ 1
2696
- ์ƒ์‹์ ์ธ 1
2697
- ๊ฑฐ์Šค๋ฅด๊ณ  1
2698
- ์ƒ์‡„๋˜๊ณ  1
2699
- ์‹ฌ๊ฐํ•ด์กŒ๋‹ค 1
2700
- ์ง€์ง€์ž๋“ค 1
2701
- ๊ทน๋‹จ์ ์ธ 1
2702
- ๋‚ด๋กœ๋‚จ๋ถˆ๋กœ 1
2703
- ์ฝ˜ํฌ๋ฆฌํŠธ 1
2704
- ์ƒํ™œ์ธ๋“ค 1
2705
- ๋Œ์•„์„œ๊ฒŒ 1
2706
- ์ตœ๋Œ€์น˜์ธ 1
2707
- ๊ฑท์–ด๋ถ™์ด๊ณ  1
2708
- ๊ธˆ์œต์œ„์›ํšŒ 1
2709
- ์ •์ฑ…์‹ค์žฅ 1
2710
- ๊ฒฝ์ œ์ˆ˜์„ 1
2711
- ํ‹€์–ด์ฅ๊ณ  1
2712
- ๋ถˆ๋Ÿฌ์˜ค๊ธด 1
2713
- ๊ฐ€๊ณ„๋ถ€์ฑ„ 1
2714
- ํ™˜๊ธฐ์‹œํ‚ฌ 1
2715
- ๊ธˆ์œต๊ฐ๋…์›์žฅ์ผ 1
2716
- ์•„์šฐ์„ฑ์น˜ 1
2717
- ์žฅ์ฐจ๊ด€๋“ค 1
2718
- ๋›ฐ์–ด๋“ค์–ด 1
2719
- ๋ˆ์งˆ์ง€๊ฒŒ 1
2720
- ๋…ธ๋ ฅํ•˜๊ณ  1
2721
- ์‹๋‹น์ฃผ๋“ค 1
2722
- ์‹ ์Œํ•˜๊ณ  1
2723
- ๋งˆ๋ จํ•˜๋ ค๊ณ  1
2724
- ํด๋Ÿฌ์Šคํ„ฐ 1
2725
- ์ดˆ๋ž˜ํ• ์ง€ 1
2726
- ์†ก๋ฐฐ์ „๋ง 1
2727
- ํ•ด๊ฒฐ๋˜๊ณ  1
2728
- ์ถ”์ง„ํ•˜๋‹ค 1
2729
- ๋งž์ถ”์—ˆ๊ฑฐ๋‚˜ 1
2730
- ์‚๋—ํ–ˆ๋‹ค๊ฐ„ 1
2731
- ์ฃผ๋„์ ์œผ๋กœ 1
2732
- ๋‹น์—ฐํ•˜๋‹ค 1
2733
- ์‚ฌ๋žŒ์ด๋ผ 1
2734
- ๋‘๋“œ๋Ÿฌ์ง€ 1
2735
- ๋“ค์—ˆ๋‹ค๊ณ  1
2736
- ์ •๊ถŒ์—์„œ 1
2737
- ๋ถ„๋ฅ˜๋˜๊ธธ 1
2738
- ํ˜„์•ˆ์ด๋ผ 1
2739
- ๋งํ•ด์ค€๋‹ค 1
2740
- ์ƒ์ธต๋ถ€์—์„œ 1
2741
- ์‚ฌ๋ผ์ง€๋ฉด 1
2742
- ๋งŒ๋ฌดํ•˜๋‹ค 1
2743
- ์ฑ…์ž„์ง€๊ณ  1
2744
- ์žฅ๋ฒฝ์œผ๋กœ 1
2745
- ์•„๋ฉ”๋„ดํ—คํŠธ 1
2746
- ๋ฐ”๋นŒ๋กœ๋‹ˆ์•„ 1
2747
- ๋„ค๋ถ€์นด๋“œ๋„ค์ž๋ฅด 1
2748
- ํ† ๋ชฉ๊ณต์‚ฌ๋ผ 1
2749
- ์–ธ์ œ๊นŒ์ง€๋‚˜ 1
2750
- ๋ง‰์•„์ฃผ์ง€ 1
2751
- ๋ฐฉ์ง€์šฉ์œผ๋กœ 1
2752
- ํ…Œ๋Ÿฌ๋‹จ์ฒด 1
2753
- ์„ผ์„œ๊นŒ์ง€ 1
2754
- ์š”๋ฅด๋‹จ๊ฐ• 1
2755
- ํ•˜๋“œ๋ฆฌ์•„๋ˆ„์Šค 1
2756
- ๋ณ€์งˆ๋๋‹ค 1
2757
- ๊ตฌ์ถ•ํ–ˆ๋‹ค 1
2758
- ์„œ๋…์œผ๋กœ 1
2759
- ๋„˜์œผ๋ ค๋‹ค 1
2760
- ์•„ํ”„๊ฐ€๋‹ˆ์Šคํƒ„ 1
2761
- ๋ด‰์‡„ํ•˜๊ณ  1
2762
- ๋ชจ์–‘์ด๋‹ค 1
2763
- ์žฅ๋ฒฝ์ด๋‹ค 1
2764
- ์ฝ˜ํŠธ๋ฆฌํŠธ 1
2765
- ๊ฐ์˜ฅ์œผ๋กœ 1
2766
- ๋กœ๋งˆ์ธ๋“ค 1
2767
- ๊ฐ€๋Šฅํ•˜์ง€ 1
2768
- ๊ด‘์ฃผ๊ด‘์—ญ์‹œ 1
2769
- ์กธ์—…ํ•˜๋˜ 1
2770
- ์‹œ์ธ์œผ๋กœ 1
2771
- ๋“ฑ๋‹จํ–ˆ๋‹ค 1
2772
- ์ด๋“ฌํ•ด์ธ 1
2773
- ๋‹จํŽธ์†Œ์„ค 1
2774
- ์„œ์šธ์‹ ๋ฌธ 1
2775
- ์‹ ์ถ˜๋ฌธ์˜ˆ 1
2776
- ์†Œ์„ค๊ฐ€๋กœ 1
2777
- ๋ฐ๋ท”ํ–ˆ๋‹ค 1
2778
- ์ž‘๊ฐ€์ด๊ฑฐ๋‚˜ 1
2779
- ํ‰๋ก ๊ฐ€์ธ 1
2780
- ์ด๋ฆ„์ด๋ผ 1
2781
- ํ•œ๋™๋ฆผ์”จ 1
2782
- ์†Œ์„ค๊ฐ€์ด๊ณ  1
2783
- ๊ฒฝํฌ์‚ฌ์ด๋ฒ„๋Œ€ 1
2784
- ๋ฏธ๋””์–ด๋ฌธ์˜ˆ์ฐฝ์ž‘ 1
2785
- ๋ฌธํ•™ํ‰๋ก ๊ฐ€๋‹ค 1
2786
- ๋ฌธ์˜ˆ์ฐฝ์ž‘๊ณผ 1
2787
- ๋™๋ฆฌ๋ฌธํ•™์ƒ 1
2788
- ํ™ฉ์ˆœ์›๋ฌธํ•™์ƒ 1
2789
- ๋ฐ›์•˜๋Š”๋ฐ 1
2790
- ํ•œ์Šน์›์”จ 1
2791
- ์ข‹์•„ํ–ˆ๋‹ค 1
2792
- ๊ณ ๋“ฑํ•™๊ต 1
2793
- ์“ฐ๊ฒ ๋‹ค๋ฉฐ 1
2794
- ์„ ํƒํ•˜๋”๋‹ˆ 1
2795
- ์‹œ๋‹ฌ๋ฆฌ๋‹ค 1
2796
- ๊ฑฐ๋ถ€ํ•˜๊ฒŒ 1
2797
- ๋˜์–ด๊ฐ„๋‹ค๊ณ  1
2798
- ํŠธ๋ผ์šฐ๋งˆ๋กœ 1
2799
- ์‹œ์„ ์—์„œ 1
2800
- ์—ฐ์ž‘์†Œ์„ค 1
2801
- ๋ฒ ์ŠคํŠธ์…€๋Ÿฌ๋‹ค 1
2802
- ์ˆ˜์ƒํ–ˆ๋‹ค 1
2803
- ๋Œ€์ค‘์†Œ์„ค๊ฐ€์ด๊ธฐ๋ณด๋‹ค 1
2804
- ์ „์œ„์†Œ์„ค๊ฐ€๋‹ค 1
2805
- ์ฒ˜์Œ๋ถ€ํ„ฐ 1
2806
- ๋‹ค๋ฃธ์œผ๋กœ์จ 1
2807
- ์ฃผ๋ชฉ๋ฐ›์ง€ 1
2808
- ํ—ˆ๋•์ด๋ฉด์„œ 1
2809
- ์ˆœ๊ฐ„๋ถ€ํ„ฐ 1
2810
- ์ถ•์ ํ•ด์˜จ 1
2811
- ์ผ์–ด๋‚˜๊ธฐ 1
2812
- ์ด์‚ฌํ–ˆ๋˜ 1
2813
- ์‹œ๋ฐœ์ ์œผ๋กœ 1
2814
- ๋“ค์–ด์™”๋‹ค 1
2815
- ์ฃผ์ œ์˜์‹ 1
2816
- ์ž‘ํ’ˆ์œผ๋กœ 1
2817
- ๋ฌด๊ณ ํ•˜๊ฒŒ 1
2818
- ๊ฐ•์—ฐ์—์„œ 1
2819
- ๋“ค์—ฌ๋‹ค๋ณด๋ฉด 1
2820
- ๋งน์„ธ์ด์ž 1
2821
- ๊ถ๊ทน์ ์ธ 1
2822
- ์‹œ์„ ์œผ๋กœ 1
2823
- ํ’€์–ด๋ƒˆ๋‹ค 1
2824
- ๋ฌธํ•™์ƒ์ธ 1
2825
- ์›€๋ฒ ๋ฅดํ†  1
2826
- ์ฒ˜์Œ์ด์—ˆ๋‹ค 1
2827
- ์˜๋ฃŒ์ง„๋“ค 1
2828
- ์•Š์•˜๋Š”๋ฐ 1
2829
- ๋ณด๊ฑด๋ณต์ง€์œ„์›ํšŒ 1
2830
- ์ง•๊ณ„์œ„์›ํšŒ 1
2831
- ์ƒํƒœ์ด๋ฉฐ 1
2832
- ์ธ์‚ฌ์œ„์›ํšŒ 1
2833
- ๊ตญ๋ฏผ๊ถŒ์ต์œ„์›ํšŒ 1
2834
- ์กฐ์น˜์ด๋‹ค 1
2835
- ํŠนํ˜œ์ด๋ฉฐ 1
2836
- ๊ฒƒ์ด๋ฏ€๋กœ 1
2837
- ๋Œ€์ƒ์ด๋ผ๊ณ  1
2838
- ์ฒ˜๋ฆฌํ–ˆ๋‹ค 1
2839
- ์„œ์šธ๋Œ€๋ณ‘์›ํ–‰ 1
2840
- ์ˆ˜์ˆ ํ•˜๋ฉด 1
2841
- ๋ฌธ์˜ํ•˜์ž 1
2842
- ์ˆ˜์ˆ ๋ฐ›๊ธฐ 1
2843
- ์˜ํ•™์ ์œผ๋กœ 1
2844
- ์ถœ๋™ํ•˜๋„๋ก 1
2845
- ๋ช…ํ™•ํ•˜๊ฒŒ 1
2846
- ์žฌ์ •๋น„ํ•ด์•ผ 1
2847
- ์‹ฌ์žฅ๋งˆ๋น„๋กœ 1
2848
- ๋ณต๊ท€ํ–ˆ๊ณ  1
2849
- ์„ ๋ฐœ์—์„œ 1
2850
- ์ง€๋ช…๋๋‹ค 1
2851
- ๋‚ด๋น„์ณค๊ธฐ 1
2852
- ํ˜„์žฌ๊นŒ์ง€ 1
2853
- ์„œ๋จธ๋ฆฌ๊ทธ 1
2854
- ์ •๋„๋ผ๋ฉด 1
2855
- ๋ฏธ๋„ค์†Œํƒ€ 1
2856
- ํŒ€๋ฒ„์šธ๋ธŒ์Šค 1
2857
- ๋งž๋Œ€๊ฒฐ์—์„  1
2858
- ์—ญ์‚ฌ์—์„œ 1
2859
- ๊ธฐ๋ก๋๋‹ค 1
2860
- ์™ธ๋ฉดํ–ˆ๋‹ค 1
2861
- ์ „๋ฌธ๋งค์ฒด 1
2862
- ๋“œ๋ž˜ํ”„ํŠธ 1
2863
- ๋˜์–ด์žˆ์ง€ 1
2864
- ๋ ˆ์ด์ปค์Šค์—์„  1
2865
- ํ˜นํ‰ํ–ˆ๋‹ค 1
2866
- ์„ ๋ฐฐ์ด์ž 1
2867
- ๋ธŒ๋กœ๋‹ˆ๋ผ๋ฉด 1
2868
- ํ•ด๋‹ฌ๋ผ๊ณ  1
2869
- ํ•œ๊ตญ๋ฌธํ™”์žฌ๋ณด์กด๊ณผํ•™ํšŒ 1
2870
- ์ดํŒ๋งค์†Œ 1
2871
- ์ธ๋ฌธํ•™์ˆ ์› 1
2872
- ๊ฐ์›๊ต์ˆ˜ 1
2873
- ์ž์œ ๊ต์–‘ํ•™๋ถ€ 1
2874
- ๋ฐ˜๋„์ฒด์ƒ์‚ฐ์„ค๋น„ 1
2875
- ์›จ์ดํผ๋งˆ์Šคํ„ฐ 1
2876
- ๋Œ€ํ‘œ์ด๊ธฐ 1
2877
- ์†Œํ”„ํŠธ์›จ์–ด 1
2878
- ๋น„๊ตํ–ˆ๋‹ค 1
2879
- ๊ฒน์ณ๋ณด๋ฉด 1
2880
- ์ฆˆ๋ ค๋ฐŸ๊ณ  1
2881
- ๊ตฌ์ ˆ์—์„œ 1
2882
- ์กฐํŒํ•ด์„œ 1
2883
- ํ˜„์ƒ์ด๋ผ 1
2884
- ํ™œํŒ์ธ์‡„ 1
2885
- ํ•œ์ชฝ์œผ๋กœ 1
2886
- ์ธ์‡„๋œ๋‹ค 1
2887
- ํ‰์••์‹์œผ๋กœ 1
2888
- ๋ฏธ๋ฃจ์–ด๋ดค 1
2889
- ์ง„๋‹ฌ๋ž˜๊ฝƒ 1
2890
- ํ•™๊ณ„์—์„œ 1
2891
- ์ดˆ๊ฐ„๋ณธ์œผ๋กœ 1
2892
- ์ฒจ์˜ˆํ•˜๊ฒŒ 1
2893
- ๋ฌธํ•™์„œ์ง€ 1
2894
- ๋ฐœ๊ฒฌํ–ˆ๋‹ค๋ฉฐ 1
2895
- ๊ฐ™์Šต๋‹ˆ๋‹ค 1
2896
- ๋™์ฐธํ•ด์ฃผ๊ธธ 1
2897
- ๋ชฌํŠธ๋ฆฌ์˜ฌ 1
2898
- ์ธ๊ถŒ๋‹จ์ฒด 1
2899
- ๊ฐœ์ธ์‚ฌ์ •์œผ๋กœ 1
2900
- ํ™”์ƒ์—ฐ๊ฒฐ๋กœ 1
2901
- ๊ฐ•์ œ๋ถ์†ก 1
2902
- ์ฆ์–ธํ–ˆ๋‹ค 1
2903
- ๊ตญ์ œํ™œ๋™ํŒ€ 1
2904
- ์†Œ์†์œผ๋กœ 1
2905
- ์ด์•ผ๊ธฐํ–ˆ๋‹ค 1
2906
- ๋†€๋ผ์›Œํ–ˆ๋‹ค๊ณ  1
2907
- ์ค‘๊ตญ์—์„œ 1
2908
- ๋ถ์†ก์‹œํ‚จ 1
2909
- ์‚ฌ๊ณผํ•˜๊ณ  1
2910
- ๋ฐํ˜”๋‹ค๊ณ  1
2911
- ์ต์ˆ™ํ•˜์ง€ 1
2912
- ๊ฐ€๋ฅด์ณ์ฃผ๊ณ  1
2913
- ๊น€์ผํ˜์”จ 1
2914
- ํƒˆ๋ถํ–ˆ๋‹ค 1
2915
- ์œ ์—”๋ณธ๋ถ€ 1
2916
- ์•ˆ์ „๋ณด์žฅ์ด์‚ฌํšŒ 1
2917
- ๋น„ํŒํ•˜๊ธฐ 1
2918
- ์ง€์†ํ•˜๋„๋ก 1
2919
- ํ•œ๋ณด์ด์Šค 1
2920
- ๊ทœํƒ„ํ•˜๊ณ  1
2921
- ๊ณต๋™์„ฑ๋ช… 1
2922
- ์ƒ์›์˜์› 1
2923
- ์‹คํ˜„๋œ๋‹ค๋ฉด 1
2924
- ์˜๊ตฌ์ ์œผ๋กœ 1
2925
- ์„ ์–ธํ–ˆ๋‹ค 1
2926
- ์กฐ์„ ์ธ๋ฏผ๊ตฐ 1
2927
- ๋Š์–ด๋ฒ„๋ฆฌ๊ณ  1
2928
- ๋ฐฉ์–ด์ถ•์„ฑ๋ฌผ๋กœ 1
2929
- ์š”์ƒˆํ™”ํ•˜ 1
2930
- ์ „ํ™”ํ†ต์ง€๋ฌธ 1
2931
- ๊ตฐ์‚ฌํ›ˆ๋ จ 1
2932
- ํ•ต์ „๋žต์ž์‚ฐ 1
2933
- ์—ด๊ฑฐํ•˜๋ฉฐ 1
2934
- ์ด์–ด์˜ค๊ณ  1
2935
- ๋ฌผ๋ฆฌ์ ์œผ๋กœ 1
2936
- ๋ณด์—ฌ์ฃผ๊ธฐ 1
2937
- ํ•ฉ๋™์ฐธ๋ชจ๋ณธ๋ถ€ 1
2938
- ํ˜„์ƒ๋ณ€๊ฒฝ 1
2939
- ์ขŒ์‹œํ•˜์ง€ 1
2940
- ์‚ฌ๋ผ์ง€๊ณ  1
2941
- ๊ณต๊ณ ํ•ด์ง€๋ฉด์„œ 1
2942
- ๊ตฌ์ถ•ํ•˜๊ธฐ 1
2943
- ํ™œ์„ฑํ™”๋  1
2944
- ๋Œ€์‘ํ•˜๊ธฐ 1
2945
- ์ง‘์ค‘ํ•œ๋‹ค 1
2946
- ๋ฐœํ‘œํ•˜๋ฉฐ 1
2947
- ์ œ์‹œํ–ˆ๋‹ค 1
2948
- ์ €์žฅํ•˜๊ณ  1
2949
- ์ด๋™์‹œํ‚ค 1
2950
- ์ „์ฒด์—์„œ 1
2951
- ํŒ๋งคํ•˜๊ณ  1
2952
- ํ™•์žฅํ•˜๊ธฐ๋กœ 1
2953
- ์ฃผ๋ ฅ์ธ๋ฐ 1
2954
- ๋ฏธ๋“œ๋‹ˆ์ผˆ 1
2955
- ์ค‘์ €๊ฐ€ํ˜• 1
2956
- ์‹๋‹น์—๊ฒŒ 1
2957
- ์ˆ˜์ค€๊นŒ์ง€ 1
2958
- ์™„ํ™”ํ•˜๊ฒ ๋‹ค 1
2959
- ํ†ต์ œํ•˜์ž 1
2960
- ๋ฐฐ๋‹ฌ์—…๊ณ„ 1
2961
- ์œ ๋ ฅํ•˜๊ฒŒ 1
2962
- ์˜์„ธ์—…์ฒด์ผ์ˆ˜๋ก 1
2963
- ๋‚ฎ์ถฐ์ค€๋‹ค 1
2964
- ๊ตฌ์ฒด์ ์ธ 1
2965
- ํ™•์ •๋˜์ง€ 1
2966
- ์•Š์•˜์œผ๋‚˜ 1
2967
- ์ˆ˜์ˆ˜๋ฃŒ์œจ๊นŒ์ง€ 1
2968
- ๊ณ ๋ คํ•˜๊ณ  1
2969
- ๋ฌธ์ œ์˜์‹ํ•˜ 1
2970
- ์ถœ๋ฒ”ํ–ˆ๋‹ค 1
2971
- ์ขํ˜€์ง€์ง€ 1
2972
- ๊ทœ์œจํ•ด์•ผ 1
2973
- ์†Œ๊ทน์ ์ธ 1
2974
- ๊ฐ•๋ ฅํ•˜๊ฒŒ 1
2975
- ์ œ์žฌํ•ด์•ผ 1
2976
- ์ทจ์ง€์˜€๋‹ค 1
2977
- ์ƒํ•œ์ œ๋ž€ 1
2978
- ์ง€์—ญ์—์„  1
2979
- ์ ์šฉํ•˜๊ณ  1
2980
- ๋„์ž…ํ•œ๋‹ค 1
2981
- ์‹ ํ˜ธ์˜€๋‹ค 1
2982
- ์ „๋ฐฉ์œ„์  1
2983
- ๋…๊ณผ์ ์  1
2984
- ์‹๋‹น์œผ๋กœ 1
2985
- ์ด๋“ฌํ•ด์—” 1
2986
- ์ •๋ฅ ์ œ๋กœ 1
2987
- ์ „ํ™˜ํ–ˆ๋‹ค 1
2988
- ๋ฐ˜๋ฐ•ํ•œ๋‹ค 1
2989
- ์šฐ๋ฒ„์ด์ธ  1
2990
- ์ €์ŠคํŠธ์ž‡ 1
2991
- ์น˜ํ‚จ์ด๋‚˜ 1
2992
- ํ•œ์ •๋๋˜ 1
2993
- ๋‹ค์–‘ํ•ด์กŒ๋‹ค 1
2994
- ๋„“์–ด์กŒ๋‹ค 1
2995
- ๊ณผ์ ํ•˜๋ฉด์„œ 1
2996
- ์ฆ๊ฐ€์‹œํ‚จ 1
2997
- ์ง„๋‹จํ•œ๋‹ค 1
2998
- ๊ฐ๋‹นํ•˜์ง€ 1
2999
- ๋ถ€๋‹ดํ•ด์„œ 1
3000
- ๋ฐœ์˜ํ–ˆ์ง€ 1
3001
- ๋ฌด์‚ฐ๋๋‹ค 1
3002
- ํŒŒ๊ฒฌ๊ฒ€์‚ฌ 1
3003
- ํ›„๋ณด์ž์ถ”์ฒœ์œ„ 1
3004
- ๊ตฌ์„ฑ์—์„œ 1
3005
- ์ƒ์„คํŠน๊ฒ€๋ฒ•์ƒ 1
3006
- ๊ตฌ์„ฑ๋œ๋‹ค 1
3007
- ์ถ”์ฒœํ•˜๊ฒŒ 1
3008
- ์‚ผ๋ถ€ํ† ๊ฑด 1
3009
- ๋„์ด์น˜๋ชจํ„ฐ์Šค 1
3010
- ์ฃผ๊ฐ€์กฐ์ž‘ 1
3011
- ๋น ์กŒ๋Š”๋ฐ 1
3012
- ๋ณ‘ํ–‰ํ•˜๊ฒ ๋‹ค 1
3013
- ํ•ด์„๋๋‹ค 1
3014
- ํ˜•ํƒœ์ธ๋ฐ 1
3015
- ๊ฒฐํ•ฉ๋ ์ง€ 1
3016
- ๊ฒฐ์ •๋œ๋‹ค 1
3017
- ๋ถ„์„ํ•˜๋ฉด 1
3018
- ๊ฒฐํ•ฉํ•˜๋ฉด 1
3019
- ๊ฐœ๋ฐœ์ด๋‚˜ 1
3020
- ์‚ฌ์šฉ๋œ๋‹ค 1
3021
- ์ˆ˜์„์—ฐ๊ตฌ์› 1
3022
- ๋ถ„์„ํ•œ๋‹ค 1
3023
- ๋ถˆ๊ณผํ–ˆ๋‹ค 1
3024
- ์˜ˆ์ธกํ–ˆ๋‹ค 1
3025
- ๋ฐ์ด๋น„๋“œ 1
3026
- ์›Œ์‹ฑํ„ด๋Œ€ 1
3027
- ๊ฐœ๋ฐœํ–ˆ๊ณ  1
3028
- ๋‚˜๋…ธ์†Œ์žฌ 1
3029
- ์ƒ๋ช…๊ณผํ•™๋ถ€ 1
3030
- ๋กœ์ œํƒ€ํด๋“œ 1
3031
- ์ฃผ๋„ํ•˜๋ฉฐ 1
3032
- ์—ฐ๊ตฌ๋‹จ๊ณ„ 1
3033
- ํ•ด๊ฒฐํ•˜๊ฒ ๋‹ค 1
3034
- ํ™”ํ•™๋ฌผ์งˆ 1
3035
- ๋จน์–ด์น˜์šฐ 1
3036
- ๋ฐœ์ƒํ•˜์ง€ 1
3037
- ๋ฉ”ํƒ„๊ฐ€์Šค 1
3038
- ํ™”ํ•™๋น„๋ฃŒ 1
3039
- ๋†์‹ํ’ˆ๋ถ€ 1
3040
- ๊ณ„ํšํ•˜๊ณ  1
3041
- ๊ธฐ์—ฌํ•˜๊ณ  1
3042
- ๋ฌด์„ ์œผ๋กœ 1
3043
- ๋กœ๋ด‡์ฒญ์†Œ๊ธฐ์ธ 1
3044
- ๋นจ์•„๋“ค์ธ 1
3045
- ๊ฑธ๋Ÿฌ๋‚ด๊ณ  1
3046
- ๋ฉด์ ์ด๋‚˜ 1
3047
- ์œ ์šฉํ•˜๋‹ค 1
3048
- ๊ณ ์šฉ๋…ธ๋™๋ถ€ 1
3049
- ์†Œ์…œ๋ฒค์ฒ˜๊ฒฝ์—ฐ๋Œ€ํšŒ 1
3050
- ์ตœ์šฐ์ˆ˜์ƒ 1
3051
- ํ•ด์–‘์ˆ˜์‚ฐ๋ถ€ 1
3052
- ์ธ์ •๋ฐ›์•˜๋‹ค 1
3053
- ๊ฒฝ์ฐฐ์ธ์žฌ๊ฐœ๋ฐœ์› 1
3054
- ์„œํˆฌ๋ฅด๊ฒŒ 1
3055
- ๋“ค๋ ค์™”๋‹ค 1
3056
- ์ฝ์œผ๋ฉด์„œ 1
3057
- ๊ณ ์ณ์คฌ๋‹ค 1
3058
- ์†Œํ™”ํ•˜๊ณ  1
3059
- ํ™œ์„ฑํ™”ํ•˜๋ฉด์„œ 1
3060
- ๋งŒ๋“ค์–ด๋‹ฌ๋ผ๊ณ  1
3061
- ์š”์ฒญํ–ˆ๊ณ  1
3062
- ์ด์ˆ˜ํ•œ๋‹ค 1
3063
- ์•ˆํŒŽ์ด๋‹ค 1
3064
- ํˆฌ์ž…ํ•˜๊ณ  1
3065
- ๋ฒ”์ฃ„์ž๋“ค 1
3066
- ์ผ์–ด๋‚œ๋‹ค 1
3067
- ๋ฒ ํŠธ๋‚จ์œผ๋กœ 1
3068
- ๋„์ฃผํ–ˆ๋‹ค 1
3069
- ์†กํ™˜๋๋‹ค 1
3070
- ๋„ํ”ผํ–ˆ๋‹ค๊ณ  1
3071
- ์ˆ˜๊ฐ•์ƒ๋“ค 1
3072
- ํ•œ๊ตญ์ธ๋“ค 1
3073
- ๋‹ค๋‚ญ๊ฒฝ์ฐฐ์„œ 1
3074
- ๊ณต์•ˆ์ด๋ผ๊ณ  1
3075
- ๋ฌด์„œ์›Œํ•˜ 1
3076
- ํ•œ๊ตญ์–ด๋กœ 1
3077
- ์นœ๋ฐ€๊ฐ์ด๋ผ 1
3078
- ์‹ ์ฒญํ•˜๊ฒŒ 1
3079
- ์ข‹์•„ํ•˜๊ณ  1
3080
- ๋ฐฉํƒ„์†Œ๋…„๋‹จ 1
3081
- ๋‹ด๋‹น์ž์ธ 1
3082
- ๋‹จ๊ณ„์—์„œ 1
3083
- ์Šต๋“๋ ฅ๋“ค 1
3084
- ํ˜„์ง€์—์„œ 1
3085
- ๊ทน๋‹จ์ ์œผ๋กœ 1
3086
- ๊ธฐ์ƒํ•™์  1
3087
- ๋‹น๊ฒจ์กŒ๋‹ค 1
3088
- ๋Šฆ์–ด์กŒ๋‹ค 1
3089
- ๊ธฐ์ƒํ•™์ ์œผ๋กœ 1
3090
- ์—ฌ๋ฆ„์ด๊ณ  1
3091
- ๋นผ์•—๊ธฐ๊ณ  1
3092
- ๊ธธ์–ด์ง€๋ฉด์„œ 1
3093
- ์˜ฌ๋ผ๊ฐ€๊ณ  1
3094
- ๊ธธ์–ด์ง€๊ธฐ 1
3095
- ๋…ํ•ด์ง€๊ธฐ 1
3096
- ์ƒ์Šน์œผ๋กœ 1
3097
- ์˜ค์ „๋ถ€ํ„ฐ 1
3098
- ์น˜์†Ÿ์•˜๋˜ 1
3099
- ์›์ธ์œผ๋กœ 1
3100
- ๋“ฑ์žฅ์œผ๋กœ 1
3101
- ์ƒ‰๊น”์ด๊ธฐ 1
3102
- ์—ญํ• ๊นŒ์ง€ 1
3103
- ๋งก์•„๊ฐ€๋ฉฐ 1
3104
- ์ง€ํœ˜ํ–ˆ๋‹ค 1
3105
- ๋ถ€์œ„์›์žฅ 1
3106
- ์—ฐ์ถœํ•˜๊ณ  1
3107
- ํ•œ๊ตญ์˜ˆ์ˆ ์ข…ํ•ฉํ•™๊ต 1
3108
- ๊ฐœ๋ง‰์ž‘์ธ 1
3109
- ์žฌ๋ฏธ์žˆ๊ฒŒ 1
3110
- ์ž‘๋…„๊นŒ์ง€ 1
3111
- ๋‚ด์šฐ์™ธํ™˜ 1
3112
- ์ง„ํ–‰๋๋‚˜ 1
3113
- ํ”„๋กœ๊ทธ๋ž˜๋จธ 1
3114
- ๋ถ€๊ตญ์ œ๋ฐ– 1
3115
- ๊ณต์„์ด๋‹ค 1
3116
- ๋น„ํ˜„์‹ค์  1
3117
- ์‹œ์ž‘ํ•ด์„œ 1
3118
- ์ง€๋‚˜์น˜๋‹ค 1
3119
- ์˜ํ™”์ œ์ธ 1
3120
- ์™ธ๋ฉดํ•˜๋А๋ƒ 1
3121
- ์ž๋ณธ์ด๋ผ์„œ 1
3122
- ์Ÿ์•„์กŒ์„๊นŒ 1
3123
- ์„ฑ์‚ฌ๋๋‹ค 1
3124
- ์˜ฅ์™ธ๊ด‘๊ณ ๋ฌผ๋ฒ• 1
3125
- ๊ตฌ์ฒญ์—์„œ 1
3126
- ๋ฐ›์•„์˜ค๋ฉด 1
3127
- ์˜ํ™”์ œ๋ผ 1
3128
- ๊ฐ€๋Šฅํ–ˆ๋˜ 1
3129
- ๊ณ„์•ฝ์ด๊ธฐ 1
3130
- ๋‚ด๋…„๋Œ€๋กœ 1
3131
- ์ „๊ตญ์ฒด์ „ 1
3132
- ์˜ฎ๊ธฐ๊ธฐ๋กœ 1
3133
- ์žฌ๋ฏธ์—†์œผ๋ฉด 1
3134
- ๊ด€๊ฐ์—๊ฒŒ 1
3135
- ์™ธ๋ฉด๋‹นํ•œ๋‹ค 1
3136
- ๋– ๋‚˜๊ฐ€๊ฒŒ 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/resources/{vocabulary_v2.csv โ†’ vocabulary.csv} RENAMED
@@ -144,74 +144,4 @@
144
  12ยท12 ๊ณ„์—„,12ยท3 ๊ณ„์—„,
145
  ์ง„์ฒ™์œจ,์ง„์ฒ™๋ฅ ,
146
  ๊ฐฑ์‹ ๋ฅ ,๊ฐฑ์‹ ์œจ,
147
- ์šธ๋ž€๋ฐ”ํ† ๋ฅด,์šธ๋ž€๋ฐ”ํƒ€๋ฅด,์œ—์ธต,์œ„์ธต,
148
- ์•„๋žซ์ธต,์•„๋ž˜์ธต,
149
- ์œ—์ชฝ,์œ„์ชฝ,
150
- ์•„๋žซ์ชฝ,์•„๋ž˜์ชฝ,
151
- ๋’ท์ชฝ,๋’ค์ชฝ,
152
- ๊ฐฏ์ˆ˜,๊ฐœ์ˆ˜,
153
- ์ด›์ ,์ดˆ์ ,
154
- ๋Œ“๊ฐ€,๋Œ€๊ฐ€,
155
- ์‹ฏ๊ฐ€,์‹œ๊ฐ€,
156
- ํ—›์ ,ํ—ˆ์ ,
157
- ์ž‡์ ,์ด์ ,
158
- ์ˆซ๊ฐ€๋ฝ,์ˆŸ๊ฐ€๋ฝ,
159
- ๊น๋šœ๊ธฐ,๊น๋‘๊ธฐ,
160
- ์ˆ˜๋„๋ฌผ,์ˆ˜๋—๋ฌผ,
161
- ์กด๋Œ€๋ง,์กด๋Œ“๋ง,
162
- ์ธ์‚ฟ๋ง,์ธ์‚ฌ๋ง,
163
- ํ˜ผ์ž๋ง,ํ˜ผ์žฃ๋ง,
164
- ๋…ธ๋ž˜๋ง,๋…ธ๋žซ๋ง,
165
- ์ตœ์†Œ๊ฐ’,์ตœ์†Ÿ๊ฐ’,
166
- ์ ˆ๋Œ€๊ฐ’,์ ˆ๋Œ“๊ฐ’,
167
- ๋“ฑ๊ต๊ธธ,๋“ฑ๊ตฃ๊ธธ,
168
- ํ•˜๊ต๊ธธ,ํ•˜๊ตฃ๊ธธ,
169
- ๋งŒ๋‘๊ตญ,๋งŒ๋‘ฃ๊ตญ,
170
- ์ˆœ๋Œ€๊ตญ,์ˆœ๋Œ“๊ตญ,
171
- ๋ถ์–ด๊ตญ,๋ถ์—‡๊ตญ,
172
- ๋’ทํ’€์ด,๋’คํ’€์ด,
173
- ์˜ค๋žซ๋งŒ,์˜ค๋žœ๋งŒ,
174
- ์˜ค๋žœ๋™์•ˆ,์˜ค๋žซ๋™์•ˆ,
175
- ์›ฌ์ง€,์™ ์ง€,
176
- ์™ ๋งŒ,์›ฌ๋งŒ,
177
- ์™ ์ผ,์›ฌ์ผ,
178
- ์™ ๊ฑธ,์›ฌ๊ฑธ,
179
- ๊ธˆ์ƒˆ,๊ธˆ์„ธ,
180
- ์–ด์˜์—†,์–ด์ด์—†,
181
- ํฌ์•ˆํ•˜,ํฌํ•œํ•˜,
182
- ๊ณฐ๊ณฐํžˆ,๊ณฐ๊ณฐ์ด,
183
- ์ผ์ผํžˆ,์ผ์ผ์ด,
184
- ํ‹ˆํ‹ˆํžˆ,ํ‹ˆํ‹ˆ์ด,
185
- ๊นจ๋—ํžˆ,๊นจ๋—์ด,
186
- ๋ช‡์ผ,๋ฉฐ์น ,
187
- ์—ญํ™œ,์—ญํ• ,
188
- ํ• ๋ ค๊ณ ,ํ•˜๋ ค๊ณ ,
189
- ๊ท€๋œธ,๊ท€๋”,
190
- ๊ท€๋ฐ,๊ท€๋”,
191
- ๋ฐœ์ž์šฑ,๋ฐœ์ž๊ตญ,
192
- ๋ˆˆ๊ผฝ,๋ˆˆ๊ณฑ,
193
- ๊ณฑ๋ฐฐ๊ธฐ,๊ณฑ๋นผ๊ธฐ,
194
- ๋ฎํžˆ,๋ฎ์ด,
195
- ๋ฎํ˜€,๋ฎ์—ฌ,
196
- ํ†ต์ฑ„๋กœ,ํ†ต์งธ๋กœ,
197
- ํ•˜๋งˆํŠธ๋ฉด,ํ•˜๋งˆํ„ฐ๋ฉด,
198
- ์–ด๋ฌผ์ฉก,์–ด๋ฌผ์ฉ,
199
- ์›…ํผ,์›€ํผ,
200
- ์ผ์‚ฌ๋ถ„๋ž€,์ผ์‚ฌ๋ถˆ๋ž€,
201
- ์ ˆ๋Œ€์ ˆ๋ช…,์ ˆ์ฒด์ ˆ๋ช…,
202
- ํ™€ํ™€๋‹จ์‹ ,ํ˜ˆํ˜ˆ๋‹จ์‹ ,
203
- ๋™๊ฑฐ๋™๋ฝ,๋™๊ณ ๋™๋ฝ,
204
- ์„ฑ๋Œ€๋ฌ˜์‚ฌ,์„ฑ๋Œ€๋ชจ์‚ฌ,
205
- ๋’ค์น˜๋‹ฅ๊ฑฐ๋ฆฌ,๋’ค์น˜๋‹ค๊บผ๋ฆฌ,
206
- ์•ˆ์„ฑ๋งˆ์ถค,์•ˆ์„ฑ๋งž์ถค,
207
- ๋˜๋ฌผ๋ฆผ,๋Œ€๋ฌผ๋ฆผ,
208
- ์‚ผ๊ฐ€ํ•˜,์‚ผ๊ฐ€,
209
- ์ฒผ๋ฆฐ์ง€,์ฑŒ๋ฆฐ์ง€,
210
- ๋ฆฌ๋”์‰ฝ,๋ฆฌ๋”์‹ญ,
211
- ๋ฉค๋ฒ„์‰ฝ,๋ฉค๋ฒ„์‹ญ,
212
- ํŒŒํŠธ๋„ˆ์‰ฝ,ํŒŒํŠธ๋„ˆ์‹ญ,
213
- ์ปจํ…์ธ ,์ฝ˜ํ…์ธ ,
214
- ๋ฉ”์„ธ์ง€,๋ฉ”์‹œ์ง€,
215
- ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜,์• ํ”Œ๋ฆฌ์ผ€์ด์…˜,
216
- ์ดˆ์ฝœ๋ ›,์ดˆ์ฝœ๋ฆฟ,
217
- ํŒœํ”Œ๋ ›,ํŒธํ”Œ๋ฆฟ,
 
144
  12ยท12 ๊ณ„์—„,12ยท3 ๊ณ„์—„,
145
  ์ง„์ฒ™์œจ,์ง„์ฒ™๋ฅ ,
146
  ๊ฐฑ์‹ ๋ฅ ,๊ฐฑ์‹ ์œจ,
147
+ ์šธ๋ž€๋ฐ”ํ† ๋ฅด,์šธ๋ž€๋ฐ”ํƒ€๋ฅด,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
runner.py CHANGED
@@ -1,20 +1,18 @@
1
- """๊ต์—ด ๊ตฌ์„ฑ(ํŒŒ์ดํ”„๋ผ์ธ ร— ํ”„๋กฌํ”„ํŠธ ร— ๋ชจ๋ธ)์„ ํ…์ŠคํŠธ 1๊ฑด์— ์‹คํ–‰ํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ๋Ÿฌ๋„ˆ.
2
-
3
- ๊ต์—ด ๋กœ์ง์„ ์žฌ๊ตฌํ˜„ํ•˜์ง€ ์•Š๊ณ  solar-eval ์—”์ง„์„ ๊ทธ๋Œ€๋กœ import ํ•œ๋‹ค โ€” ๋ฐ๋ชจ๊ฐ€
4
- ๋ณด์—ฌ์ฃผ๋Š” ๋™์ž‘์ด ํ‰๊ฐ€ run ๊ณผ ๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ๊ฐ™์•„์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค (์‚ฌ๋ณธ ๋กœ์ง์€
5
- ๋ฐ˜๋“œ์‹œ ๊ฐˆ๋ผ์ง„๋‹ค). ๋กœ์ปฌ์—์„œ๋Š” uv ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ solar_eval ์„, HF Space ์—์„œ๋Š”
6
- `build_space.py` ๊ฐ€ ๋ฒค๋”๋งํ•œ ์‚ฌ๋ณธ์„ ์“ด๋‹ค (sys.path ์ƒ ์Šคํฌ๋ฆฝํŠธ ์˜†์ด ๋จผ์ €๋ผ
7
- Space ์—์„œ๋Š” ๋ฒค๋” ์‚ฌ๋ณธ์ด ์ด๊ธด๋‹ค).
8
-
9
- **๊ตฌ์„ฑ์€ (pipeline, prompt, model) ํ•œ ๋ฌถ์Œ์ด๋‹ค** โ€” ๋ชจ๋ธ๋งŒ ๊ฐˆ์•„๋ผ์šฐ๋Š” ์„ ํƒ์ง€๋Š”
10
- ๋‘์ง€ ์•Š๋Š”๋‹ค. mini4 ์˜ ์ด๋“์€ ๋ชจ๋ธ ๊ต์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ ํŒŒ์ดํ”„๋ผ์ธยทํ”„๋กฌํ”„ํŠธ ๊ตฌ์กฐ ๋ณ€๊ฒฝ์—์„œ
11
- ๋‚˜์™”๊ณ (step1 ์„ ์ƒ์„ฑ์—์„œ ํŒ์ •์œผ๋กœ, step2 ๋ฅผ ์–ดํœ˜/๋ฌธ๋ฒ•์œผ๋กœ ๋ถ„ํ• , ๊ทœ์น™ ์ธต 2๊ฐœ ์ถ”๊ฐ€),
12
- ์˜› combo ํ”„๋กฌํ”„ํŠธ์— mini4 ๋งŒ ๊ฝ‚์œผ๋ฉด 2026-09-16 ์— ์ด๊ด€ ๋ถˆ๊ฐ€ ํŒ์ •์„ ๋ฐ›์€ ๊ทธ ์กฐํ•ฉ์ด
13
- ๋œ๋‹ค. ๊ทธ๋ž˜์„œ ์„ธ ์ถ•์„ `Config` ํ•˜๋‚˜๋กœ ๋ฌถ๊ณ , ๋ฐ๋ชจ๋Š” ๊ทธ ๊ตฌ์„ฑ๋งŒ ๋…ธ์ถœํ•œ๋‹ค.
14
-
15
- config ์ž์‚ฐ(ํŒŒ์ดํ”„๋ผ์ธ yamlยทํ”„๋กฌํ”„ํŠธยท์น˜ํ™˜ ์‚ฌ์ „ยทํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ)์€
16
- 1. `./assets/` โ€” HF Space ๋ ˆ์ด์•„์›ƒ (build_space.py ๊ฐ€ ์กฐ๋ฆฝ)
17
- 2. `../03-evaluation/` โ€” ๋ ˆํฌ ๋ ˆ์ด์•„์›ƒ (๋กœ์ปฌ ๊ฐœ๋ฐœ)
18
  ์ˆœ์„œ๋กœ ์ฐพ๋Š”๋‹ค.
19
  """
20
 
@@ -32,70 +30,63 @@ import yaml
32
 
33
  @dataclass(frozen=True)
34
  class Config:
35
- """๋ฐ๋ชจ๊ฐ€ ๊ณ ๋ฅผ ์ˆ˜ ์žˆ๋Š” ํ•œ ๊ตฌ์„ฑ. `base` ๋Š” pipeline ์ด extends ํ•˜๋Š” ํŒŒ์ผ(๋ฒˆ๋“ค ๋Œ€์ƒ)."""
36
 
37
  label: str
38
  pipeline: str
39
  prompt: str
40
  model: str
41
  note: str
42
- base: str | None = None
43
-
44
-
45
- #: ๋ฐ๋ชจ์— ๋…ธ์ถœ๋˜๋Š” ๊ตฌ์„ฑ. ์ง€๊ธˆ์€ ํ•˜๋‚˜๋ฟ์ด์ง€๋งŒ tuple ๋กœ ๋‘”๋‹ค โ€” `build_space.required_assets`
46
- #: ๊ฐ€ ์ด ๋ชฉ๋ก์—์„œ ๋ฒˆ๋“ค ์ž์‚ฐ์„ ์œ ๋„ํ•˜๋ฏ€๋กœ, ๊ตฌ์„ฑ์ด ๋Š˜์–ด๋„ ์กฐ๋ฆฝ ์ฝ”๋“œ๋ฅผ ๊ณ ์น  ํ•„์š”๊ฐ€ ์—†๋‹ค.
47
- CONFIGS: tuple[Config, ...] = (
48
- Config(
49
- label="solar-mini4 ํŒŒ์ดํ”„๋ผ์ธ",
50
- pipeline="pipeline_dev_v44", # = v41 + document_chunking
51
- prompt="prompt_dev_260917_vsplit",
52
- model="solar-mini4",
53
- note=(
54
- "FT ๊ต์—ด โ†’ ํŒ์ •ํ˜• step1(5KB) โ†’ ๋ฌธ๋งฅ ๊ต์ • ์–ดํœ˜/๋ฌธ๋ฒ• 2์Šคํ… โ†’ ๊ฒฐ์ •๋ก  ๊ทœ์น™ 6์ธต. "
55
- "์ž…๋ ฅ 3.3k ํ† ํฐ."
56
- ),
57
- base="pipeline_dev_v40",
58
  ),
 
59
  )
60
 
61
- #: ๋ผ๋ฒจ โ†’ Config
62
- BY_LABEL: dict[str, Config] = {c.label: c for c in CONFIGS}
63
- DEFAULT_LABEL = CONFIGS[0].label
64
 
65
- # ์—”์ง„์˜ StepCallback ๊ณผ ๊ฐ™์€ ๋ชจ์–‘: (event, index, total, step_name, doc_chunk)
66
- StepCallback = Callable[[str, int, int, str, tuple[int, int] | None], None]
67
 
68
 
69
- def find_config_dir(pipeline: str) -> Path:
70
- """๊ทธ ํŒŒ์ดํ”„๋ผ์ธ yaml ์„ ์‹ค์ œ๋กœ ๊ฐ€์ง„ ์ž์‚ฐ ๋ฃจํŠธ. Space ๋ฒˆ๋“ค์ด ๋ ˆํฌ๋ณด๋‹ค ๋จผ์ €๋‹ค."""
71
  here = Path(__file__).resolve().parent
72
  for cand in (here / "assets", here.parent / "03-evaluation"):
73
- if (cand / "pipelines" / f"{pipeline}.yaml").is_file():
74
  return cand
75
  raise FileNotFoundError(
76
- f"config ์ž์‚ฐ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค โ€” {here}/assets ๋˜๋Š” ../03-evaluation ์— "
77
- f"pipelines/{pipeline}.yaml ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (build_space.py ์ฐธ์กฐ)."
78
  )
79
 
80
 
81
- def build_pipeline(label: str = DEFAULT_LABEL) -> Any:
82
- """๊ตฌ์„ฑ ํ•˜๋‚˜์˜ ํŒŒ์ดํ”„๋ผ์ธ ์ธ์Šคํ„ด์Šค. ์•ฑ์—์„œ ๊ตฌ์„ฑ๋ณ„๋กœ 1ํšŒ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉํ•œ๋‹ค."""
83
- import solar_eval.pipelines.steps # noqa: F401 โ€” STEP_REGISTRY ๋“ฑ๋ก
84
  from solar_eval.core.dataset_loader import DatasetLoader
85
  from solar_eval.core.pipeline_compose import compose_pipeline
86
  from solar_eval.models.prompt_version import load_step_prompts
87
  from solar_eval.pipelines.registry import create_pipeline
88
 
89
- cfg = BY_LABEL[label]
90
- config_dir = find_config_dir(cfg.pipeline)
91
  pipelines_dir = config_dir / "pipelines"
92
 
93
  def load_base(name: str) -> dict[str, Any]:
94
  return yaml.safe_load((pipelines_dir / f"{name}.yaml").read_text(encoding="utf-8"))
95
 
96
- composed = compose_pipeline(load_base(cfg.pipeline), load_base=load_base)
97
- prompts = load_step_prompts(config_dir / "prompts" / cfg.prompt)
98
-
99
  return create_pipeline(
100
  "multi_step",
101
  input_fields=["original"],
@@ -111,47 +102,44 @@ def step_names(pipeline: Any) -> list[str]:
111
  return [step.name for step in pipeline.steps]
112
 
113
 
114
- async def _run_async(
115
- pipeline: Any, text: str, cfg: Config, on_step: StepCallback | None
116
- ) -> dict[str, Any]:
117
  from solar_eval.models.sample import EvalSample
 
118
  from solar_eval.providers.upstage import UpstageProvider
119
 
 
 
 
 
 
 
120
  sample = EvalSample(input={"original": text})
 
121
  start = time.monotonic()
122
- # ํ‰๊ฐ€ run ๊ณผ ๊ฐ™์€ ์กฐ๊ฑด: temp 0.0, reasoning off (60์ดˆ ์ง€์—ฐ ์˜ˆ์‚ฐ ์ œ์•ฝ)
123
  result = await pipeline.run(
124
  sample,
125
  prompts="",
126
- provider=UpstageProvider(),
127
- model=cfg.model,
128
  temperature=0.0,
129
  max_tokens=8000,
130
  reasoning_effort=None,
131
- on_step=on_step,
132
  )
133
- elapsed = time.monotonic() - start
134
  return {
135
  "output": result.output,
136
- "step_outputs": result.artifacts.get("step_outputs", {}),
137
  "usage": result.artifacts.get("usage", {}),
138
- "elapsed_s": elapsed,
139
- "pipeline_key": cfg.pipeline,
140
- "prompt_key": cfg.prompt,
141
- "model": cfg.model,
142
- "config_label": cfg.label,
143
  }
144
 
145
 
146
- def run_proofread(
147
- pipeline: Any,
148
- text: str,
149
- label: str = DEFAULT_LABEL,
150
- on_step: StepCallback | None = None,
151
- ) -> dict[str, Any]:
152
- """ํ…์ŠคํŠธ 1๊ฑด ๊ต์—ด. {output, step_outputs, usage, elapsed_s, pipeline_key, prompt_key, model, config_label}.
153
 
154
- `pipeline` ์€ ๊ฐ™์€ `label` ๋กœ `build_pipeline` ํ•œ ์ธ์Šคํ„ด์Šค์—ฌ์•ผ ํ•œ๋‹ค โ€” ์•ฑ์ด
155
- ๊ตฌ์„ฑ๋ณ„๋กœ ์บ์‹œํ•œ๋‹ค. `on_step` ์€ ์Šคํ… ์‹œ์ž‘/์ข…๋ฃŒ์™€ chunk ์ง„ํ–‰๋งˆ๋‹ค ๋ถˆ๋ฆฐ๋‹ค(UI ์ง„ํ–‰ ํ‘œ์‹œ).
156
  """
157
- return asyncio.run(_run_async(pipeline, text, BY_LABEL[label], on_step))
 
1
+ """๊ณ ๊ฐ ๋ฐ๋ชจ(HF Space) ๊ต์—ด ๊ตฌ์„ฑ์„ ํ…์ŠคํŠธ 1๊ฑด์— ์‹คํ–‰ํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ๋Ÿฌ๋„ˆ.
2
+
3
+ ๊ต์—ด ๋กœ์ง์„ ์žฌ๊ตฌํ˜„ํ•˜์ง€ ์•Š๊ณ  solar-eval ์—”์ง„์„ ๊ทธ๋Œ€๋กœ import ํ•œ๋‹ค - ๋ฐ๋ชจ๊ฐ€ ๋ณด์—ฌ์ฃผ๋Š” ๋™์ž‘์ด ํ‰๊ฐ€ run ๊ณผ
4
+ ๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ๊ฐ™์•„์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(์‚ฌ๋ณธ ๋กœ์ง์€ ๋ฐ˜๋“œ์‹œ ๊ฐˆ๋ผ์ง„๋‹ค). ๋กœ์ปฌ์—์„œ๋Š” uv ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ solar_eval ์„,
5
+ HF Space ์—์„œ๋Š” `build_space.py` ๊ฐ€ ๋ฒค๋”๋งํ•œ ์‚ฌ๋ณธ์„ ์“ด๋‹ค(sys.path ์ƒ ์Šคํฌ๋ฆฝํŠธ ์˜†์ด ๋จผ์ €๋ผ Space ์—์„œ๋Š” ๋ฒค๋”
6
+ ์‚ฌ๋ณธ์ด ์ด๊ธด๋‹ค).
7
+
8
+ ๊ตฌ์„ฑ์€ (pipeline, prompt, model) ํ•œ ๋ฌถ์Œ์ด๋‹ค. 2026-10-29 ๋ถ€ํ„ฐ FT ๊ต์—ด + Opus ๊ธฐ์‚ฌ ๊ฒ€ํ† 
9
+ (`pipeline_exp_261021_ft_hybrid`, ๊ทผ๊ฑฐ `03-evaluation/insights/261021_ft_hybrid_paragraph_article.md`)
10
+ ํ•˜๋‚˜๋งŒ ๋…ธ์ถœํ•œ๋‹ค. ๊ธฐ์‚ฌ ๊ฒ€ํ†  ์Šคํ…์€ `timely:` ๋ชจ๋ธ์ด๋ผ Timely ๋ผ์šฐํ„ฐ๋ฅผ ๊ฑฐ์นœ๋‹ค - `UPSTAGE_API_KEY` ์™€
11
+ `TIMELY_API_KEY` ๊ฐ€ ๋‘˜ ๋‹ค ์žˆ์–ด์•ผ ํ•œ๋‹ค.
12
+
13
+ config ์ž์‚ฐ(ํŒŒ์ดํ”„๋ผ์ธ yamlยทํ”„๋กฌํ”„ํŠธยท์น˜ํ™˜ ์‚ฌ์ „)์€
14
+ 1. `./assets/` - HF Space ๋ ˆ์ด์•„์›ƒ (build_space.py ๊ฐ€ ์กฐ๋ฆฝ)
15
+ 2. `../03-evaluation/` - ๋ ˆํฌ ๋ ˆ์ด์•„์›ƒ (๋กœ์ปฌ ๊ฐœ๋ฐœ)
 
 
16
  ์ˆœ์„œ๋กœ ์ฐพ๋Š”๋‹ค.
17
  """
18
 
 
30
 
31
  @dataclass(frozen=True)
32
  class Config:
33
+ """๋ฐ๋ชจ๊ฐ€ ๋…ธ์ถœํ•˜๋Š” ํ•œ ๊ตฌ์„ฑ. `assets` ๋Š” Space ๋ฒˆ๋“ค์— ๋“ค์–ด๊ฐˆ config ํŒŒ์ผ(03-evaluation ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ)."""
34
 
35
  label: str
36
  pipeline: str
37
  prompt: str
38
  model: str
39
  note: str
40
+ assets: tuple[str, ...]
41
+
42
+
43
+ CONFIG = Config(
44
+ label="FT ๊ต์—ด + Opus ๊ธฐ์‚ฌ ๊ฒ€ํ† ",
45
+ pipeline="pipeline_exp_261021_ft_hybrid",
46
+ prompt="prompt_exp_261021_ft_hybrid",
47
+ model="solar-pro4", # ๋ชจ๋ธ์„ ์ง€์ •ํ•˜์ง€ ์•Š์€ ์Šคํ…์šฉ ๊ธฐ๋ณธ๊ฐ’ - ์ด ๊ตฌ์„ฑ์˜ LLM ์Šคํ…์€ FTยทOpus ๋ฅผ ์ง์ ‘ ์ง€์ •ํ•œ๋‹ค
48
+ note=(
49
+ "FT ๊ต์—ด ๋ชจ๋ธ์ด ๋ฌธ๋‹จ ๋ฌถ์Œ๋งˆ๋‹ค 1์ฐจ ๊ต์—ด โ†’ Claude Opus ๊ฐ€ ๊ธฐ์‚ฌ ์ „์ฒด๋ฅผ ํ•œ ๋ฒˆ ๋ณด๊ณ  FT ์˜ค๊ต์ •์„ ๋˜๋Œ๋ฆฌ๊ณ  "
50
+ "์‚ฌ์‹คยท์ˆซ์žยท๋ฌธ๋งฅ ์˜ค๋ฅ˜๋ฅผ ๊ณ ์นจ โ†’ ๋”ฐ์˜ดํ‘œยท์œจ/๋ฅ ยทํ›„์ฒ˜๋ฆฌ ๊ทœ์น™."
 
 
 
 
 
51
  ),
52
+ assets=("resources/vocabulary.csv",),
53
  )
54
 
55
+ #: ํ•„์š”ํ•œ ํ‚ค. ๊ธฐ์‚ฌ ๊ฒ€ํ† (Opus)๊ฐ€ Timely ๋ฅผ ๊ฑฐ์นœ๋‹ค.
56
+ REQUIRED_KEYS = ("UPSTAGE_API_KEY", "TIMELY_API_KEY")
 
57
 
58
+ # (event, index, total, step_name) - ์Šคํ…์ด ๋๋‚  ๋•Œ๋งˆ๋‹ค "done" ์œผ๋กœ ๋ถˆ๋ฆฐ๋‹ค
59
+ StepCallback = Callable[[str, int, int, str], None]
60
 
61
 
62
+ def find_config_dir() -> Path:
63
+ """ํŒŒ์ดํ”„๋ผ์ธ yaml ์„ ์‹ค์ œ๋กœ ๊ฐ€์ง„ ์ž์‚ฐ ๋ฃจํŠธ. Space ๋ฒˆ๋“ค๏ฟฝ๏ฟฝ๏ฟฝ ๋ ˆํฌ๋ณด๋‹ค ๋จผ์ €๋‹ค."""
64
  here = Path(__file__).resolve().parent
65
  for cand in (here / "assets", here.parent / "03-evaluation"):
66
+ if (cand / "pipelines" / f"{CONFIG.pipeline}.yaml").is_file():
67
  return cand
68
  raise FileNotFoundError(
69
+ f"config ์ž์‚ฐ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค - {here}/assets ๋˜๋Š” ../03-evaluation ์— "
70
+ f"pipelines/{CONFIG.pipeline}.yaml ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (build_space.py ์ฐธ์กฐ)."
71
  )
72
 
73
 
74
+ def build_pipeline() -> Any:
75
+ """ํŒŒ์ดํ”„๋ผ์ธ ์ธ์Šคํ„ด์Šค. ์•ฑ์—์„œ 1ํšŒ ๋งŒ๋“ค์–ด ์žฌ์‚ฌ์šฉํ•œ๋‹ค."""
76
+ import solar_eval.pipelines.steps # noqa: F401 - STEP_REGISTRY ๋“ฑ๋ก
77
  from solar_eval.core.dataset_loader import DatasetLoader
78
  from solar_eval.core.pipeline_compose import compose_pipeline
79
  from solar_eval.models.prompt_version import load_step_prompts
80
  from solar_eval.pipelines.registry import create_pipeline
81
 
82
+ config_dir = find_config_dir()
 
83
  pipelines_dir = config_dir / "pipelines"
84
 
85
  def load_base(name: str) -> dict[str, Any]:
86
  return yaml.safe_load((pipelines_dir / f"{name}.yaml").read_text(encoding="utf-8"))
87
 
88
+ composed = compose_pipeline(load_base(CONFIG.pipeline), load_base=load_base)
89
+ prompts = load_step_prompts(config_dir / "prompts" / CONFIG.prompt)
 
90
  return create_pipeline(
91
  "multi_step",
92
  input_fields=["original"],
 
102
  return [step.name for step in pipeline.steps]
103
 
104
 
105
+ async def _run_async(pipeline: Any, text: str, on_step: StepCallback | None) -> dict[str, Any]:
 
 
106
  from solar_eval.models.sample import EvalSample
107
+ from solar_eval.providers import INFERENCE_ROUTES, RoutedProvider
108
  from solar_eval.providers.upstage import UpstageProvider
109
 
110
+ names = step_names(pipeline)
111
+
112
+ def hook(name: str, _sample: Any, _ctx: Any) -> None:
113
+ if on_step is not None:
114
+ on_step("done", names.index(name) + 1, len(names), name)
115
+
116
  sample = EvalSample(input={"original": text})
117
+ provider = RoutedProvider(UpstageProvider(), {k: c() for k, c in INFERENCE_ROUTES.items()})
118
  start = time.monotonic()
119
+ # ํ‰๊ฐ€ run ๊ณผ ๊ฐ™์€ ์กฐ๊ฑด: temp 0.0, reasoning off
120
  result = await pipeline.run(
121
  sample,
122
  prompts="",
123
+ provider=provider,
124
+ model=CONFIG.model,
125
  temperature=0.0,
126
  max_tokens=8000,
127
  reasoning_effort=None,
128
+ on_step=hook,
129
  )
 
130
  return {
131
  "output": result.output,
 
132
  "usage": result.artifacts.get("usage", {}),
133
+ "elapsed_s": time.monotonic() - start,
134
+ "pipeline_key": CONFIG.pipeline,
135
+ "prompt_key": CONFIG.prompt,
136
+ "model": CONFIG.model,
 
137
  }
138
 
139
 
140
+ def run_proofread(pipeline: Any, text: str, on_step: StepCallback | None = None) -> dict[str, Any]:
141
+ """ํ…์ŠคํŠธ 1๊ฑด ๊ต์—ด. {output, usage, elapsed_s, pipeline_key, prompt_key, model}.
 
 
 
 
 
142
 
143
+ `on_step` ์€ ์Šคํ…์ด ๋๋‚  ๋•Œ๋งˆ๋‹ค ("done", ๋ช‡ ๋ฒˆ์งธ, ์ „์ฒด, ์Šคํ… ์ด๋ฆ„) ์œผ๋กœ ๋ถˆ๋ฆฐ๋‹ค(UI ์ง„ํ–‰ ํ‘œ์‹œ).
 
144
  """
145
+ return asyncio.run(_run_async(pipeline, text, on_step))
solar_eval/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (394 Bytes). View file
 
solar_eval/__pycache__/_version.cpython-312.pyc ADDED
Binary file (233 Bytes). View file
 
solar_eval/__pycache__/config.cpython-312.pyc ADDED
Binary file (1.27 kB). View file
 
solar_eval/cli/commands/compare.py CHANGED
@@ -1,42 +1,105 @@
1
- """Compare runs side by side."""
 
 
 
 
 
 
 
 
 
 
2
 
3
  import json
 
 
4
 
5
  import click
6
  from tabulate import tabulate
7
 
 
 
 
 
8
 
9
- @click.command("compare")
10
- @click.argument("project")
11
- @click.option("--task", "-t", default=None, help="Filter by task name")
12
- @click.option(
13
- "--runs",
14
- "run_ids",
15
- default=None,
16
- help="Comma-separated run IDs to compare",
17
  )
18
- @click.option("--last", "-n", default=None, type=int, help="Compare last N runs for a task")
19
- @click.pass_context
20
- def compare_command(ctx, project, task, run_ids, last):
21
- """Compare evaluation scores across runs.
22
 
23
- Examples:
24
- solar-eval compare my-project --task ci-v1-all --last 5
25
- solar-eval compare my-project --runs "run1,run2"
26
- """
27
- cfg = ctx.obj["config"]
28
- if cfg.is_remote:
29
- raise click.ClickException("Compare is local-mode only")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- artifacts_dir = cfg.artifacts_dir(project)
32
- if not artifacts_dir.exists():
33
- click.secho("No artifacts found.", fg="yellow")
34
- return
35
 
36
- # Collect runs to compare
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  if run_ids:
38
- # Explicit run IDs
39
- run_dirs = []
40
  for rid in run_ids.split(","):
41
  rid = rid.strip()
42
  d = artifacts_dir / rid
@@ -45,33 +108,25 @@ def compare_command(ctx, project, task, run_ids, last):
45
  else:
46
  click.secho(f" Run not found: {rid}, skipping", fg="yellow")
47
  else:
48
- # Scan all runs, optionally filtered by task
49
- run_dirs = []
50
  for d in sorted(artifacts_dir.iterdir()):
51
  if not d.is_dir() or not (d / "run.json").exists():
52
  continue
53
- if task:
54
- run_meta = json.loads((d / "run.json").read_text())
55
- if run_meta.get("task") != task:
56
- continue
57
  run_dirs.append(d)
58
-
59
  if last and last < len(run_dirs):
60
  run_dirs = run_dirs[-last:]
 
61
 
62
- if not run_dirs:
63
- click.secho("No runs found to compare.", fg="yellow")
64
- return
65
 
66
- # Load run metadata and evaluation data
 
67
  runs_info = []
68
- all_categories = set()
69
  for d in run_dirs:
70
- run_meta = json.loads((d / "run.json").read_text())
71
  eval_file = d / "evaluation.json"
72
  if not eval_file.exists():
73
  continue
74
- eval_data = json.loads(eval_file.read_text())
75
  runs_info.append(
76
  {
77
  "run_id": d.name,
@@ -79,18 +134,19 @@ def compare_command(ctx, project, task, run_ids, last):
79
  "model": run_meta.get("model", "-"),
80
  "prompt": run_meta.get("prompt", run_meta.get("prompt_version", "-")),
81
  "pipeline": run_meta.get("pipeline", "-"),
82
- "eval_data": eval_data,
 
83
  }
84
  )
85
- all_categories.update(eval_data.get("scores", {}).keys())
86
 
87
- if not runs_info:
88
- click.secho("No evaluation data available for comparison.", fg="yellow")
89
- return
90
 
91
- # Build comparison table
 
 
 
 
92
  headers = ["Category"] + [f"{r['prompt']}/{r['model']}" for r in runs_info]
93
-
94
  rows = []
95
  for cat in sorted(all_categories):
96
  row = [cat]
@@ -98,26 +154,31 @@ def compare_command(ctx, project, task, run_ids, last):
98
  score = r["eval_data"].get("scores", {}).get(cat)
99
  row.append(f"{score:.4f}" if score is not None else "-")
100
  rows.append(row)
101
-
102
- # Separator and overall row
103
  rows.append(["โ”€" * 20] + ["โ”€" * 8] * len(runs_info))
104
  overall_row = ["Overall"]
105
  for r in runs_info:
106
  overall = r["eval_data"].get("overall_score")
107
  overall_row.append(f"{overall:.4f}" if overall is not None else "-")
108
  rows.append(overall_row)
 
109
 
110
- task_label = task or "all tasks"
111
- click.secho(f"\nComparison: {project}/{task_label}", fg="blue", bold=True)
112
 
113
- # Show run details
 
 
 
 
114
  for r in runs_info:
115
- click.echo(f" {r['prompt']} ({r['model']}, {r['pipeline']})")
116
- click.echo()
 
 
 
 
 
117
 
118
- click.echo(tabulate(rows, headers=headers, tablefmt="simple"))
119
 
120
- # Highlight best
121
  best_run = max(runs_info, key=lambda r: r["eval_data"].get("overall_score", 0))
122
  best_score = best_run["eval_data"].get("overall_score", 0)
123
  click.secho(
@@ -125,3 +186,45 @@ def compare_command(ctx, project, task, run_ids, last):
125
  fg="green",
126
  bold=True,
127
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compare runs side by side.
2
+
3
+ ๋น„๊ต ๋Œ€์ƒ run ๋“ค์˜ ํ–‰ ์„ ํƒ(`run.json` `dataset.path`ยท`selection`ยท`rows_selected`)์ด ์„œ๋กœ ๋‹ค๋ฅด๋ฉด ๊ฐ run ์˜
4
+ ์„ ํƒ์„ ํ—ค๋”์— ์‹ฃ๊ณ  "๋‹ค๋ฅธ ํ–‰ ์ง‘ํ•ฉ์˜ ์ ์ˆ˜๋‹ค" ๊ฒฝ๊ณ ๋ฅผ ๋‚ธ๋‹ค(๊ณ„ํš 2026-09-24-phase1a D19) โ€” `--where` ์Šฌ๋ผ์ด์Šค
5
+ run ๊ณผ ์ „์ฒด์…‹ run ์˜ ์ ์ˆ˜๊ฐ€ ๊ฐ™์€ ํ‘œ์—์„œ ์กฐ์šฉํžˆ ๋‚˜๋ž€ํžˆ ์„œ์ง€ ์•Š๊ฒŒ. quick-run ์ด ์งํ›„์— ๋ถ€๋ฅด๋Š” compare ๋„
6
+ ๊ฐ™์€ ๊ฒฝ๊ณ ๋ฅผ ๋ฐ›๋Š”๋‹ค. ์„ ํƒ์ด ๋ชจ๋‘ ๊ฐ™์œผ๋ฉด ์ถœ๋ ฅ์€ ์˜ˆ์ „ ๊ทธ๋Œ€๋กœ๋‹ค.
7
+
8
+ ๋ฐ์ดํ„ฐ์…‹ ํŒŒ์ผ ๋ฐ”์ดํŠธ(`dataset.sha256`)๋งŒ ๋‹ค๋ฅธ ๊ฒฝ์šฐ๋Š” **์„ ํƒ ์ฐจ์ด๊ฐ€ ์•„๋‹ˆ๋‹ค** โ€” ํ–‰ ์ถœ์ฒ˜ ํ‚ค๋ฅผ ๋”ํ•œ ์žฌ๋นŒ๋“œ์ฒ˜๋Ÿผ
9
+ ํ–‰ ์ง‘ํ•ฉ์€ ๊ฐ™๊ณ  ๋ฐ”์ดํŠธ๋งŒ ๋ฐ”๋€” ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋ž˜๋„ ์ •๋‹ต์ด ๋ฐ”๋€Œ์—ˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋”ฐ๋กœ ๊ฒฝ๊ณ ํ•˜๊ณ , ๋ผ๋ฒจ์— sha ์•ž์ž๋ฆฌ๋ฅผ
10
+ ์‹ค์–ด ์‚ฌ๋žŒ์ด ์ด์œ ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.
11
+ """
12
 
13
  import json
14
+ from pathlib import Path
15
+ from typing import Any
16
 
17
  import click
18
  from tabulate import tabulate
19
 
20
+ #: ๋น„๊ต ๋Œ€์ƒ ํ–‰ ์ง‘ํ•ฉ์ด ๋‹ค๋ฅผ ๋•Œ์˜ ๊ฒฝ๊ณ .
21
+ DIFFERENT_ROWS_WARNING = (
22
+ " โš  ๋น„๊ต ๋Œ€์ƒ run ๋“ค์˜ ํ–‰ ์„ ํƒ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค โ€” ๋‹ค๋ฅธ ํ–‰ ์ง‘ํ•ฉ์˜ ์ ์ˆ˜๋ผ ์ง์ ‘ ๋น„๊ตํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
23
+ )
24
 
25
+ #: ํ–‰ ์„ ํƒ์€ ๊ฐ™์€๋ฐ ๋ฐ์ดํ„ฐ์…‹ ํŒŒ์ผ ๋ฐ”์ดํŠธ๊ฐ€ ๋‹ค๋ฅผ ๋•Œ์˜ ๊ฒฝ๊ณ .
26
+ DIFFERENT_BYTES_WARNING = (
27
+ " โš  ํ–‰ ์„ ํƒ์€ ๊ฐ™์ง€๋งŒ ๋ฐ์ดํ„ฐ์…‹ ํŒŒ์ผ(sha256)์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค โ€” ํ–‰ ๋‚ด์šฉ(์ •๋‹ต ๋“ฑ)์ด ๋ฐ”๋€Œ์—ˆ์„ ์ˆ˜ ์žˆ์œผ๋‹ˆ "
28
+ "์ ์ˆ˜๋ฅผ ๋น„๊ตํ•˜๊ธฐ ์ „์— ๋‘ ํŒŒ์ผ์˜ ์ฐจ์ด๋ฅผ ํ™•์ธํ•˜์„ธ์š”."
 
 
 
 
29
  )
 
 
 
 
30
 
31
+ #: ๋ผ๋ฒจ์— ์‹ฃ๋Š” sha256 ์•ž์ž๋ฆฌ ์ˆ˜ โ€” legacy `prompt_sha` ํ‘œ์‹œ์™€ ๋งž์ถ˜๋‹ค.
32
+ SHA_DISPLAY_CHARS = 12
33
+
34
+
35
+ def _dataset(run_meta: dict[str, Any]) -> dict[str, Any] | None:
36
+ dataset = run_meta.get("dataset")
37
+ return dataset if isinstance(dataset, dict) else None
38
+
39
+
40
+ def _selection_signature(run_meta: dict[str, Any]) -> str | None:
41
+ """ํ–‰ ์ง‘ํ•ฉ์˜ ๋น„๊ต ํ‚ค โ€” `dataset` ๋ธ”๋ก์ด ์—†๋Š” ์˜› run ์€ None(๋ชจ๋ฆ„). ํŒŒ์ผ ๋ฐ”์ดํŠธ(sha)๋Š” ๋„ฃ์ง€ ์•Š๋Š”๋‹ค."""
42
+ dataset = _dataset(run_meta)
43
+ if dataset is None:
44
+ return None
45
+ key = {
46
+ "path": dataset.get("path"),
47
+ "selection": dataset.get("selection"),
48
+ "rows_selected": dataset.get("rows_selected"),
49
+ }
50
+ return json.dumps(key, sort_keys=True, ensure_ascii=False)
51
+
52
+
53
+ def _is_filtered(run_meta: dict[str, Any]) -> bool:
54
+ selection = (_dataset(run_meta) or {}).get("selection") or {}
55
+ return selection.get("where") is not None or selection.get("limit") is not None
56
 
 
 
 
 
57
 
58
+ def _selection_label(run_meta: dict[str, Any], show_sha: bool = False) -> str:
59
+ """์‚ฌ๋žŒ์šฉ ์„ ํƒ ํ‘œ๊ธฐ: `where tags=a ยท 12/159 rows` ยท `all 159/159 rows` ยท `selection unknown`.
60
+
61
+ `show_sha` ๋ฉด ` ยท sha <12>` ๋ฅผ ๋ง๋ถ™์ธ๋‹ค(ํŒŒ์ผ ๋ฐ”์ดํŠธ๊ฐ€ ๋‹ค๋ฅธ run ๋“ค์„ ๋น„๊ตํ•  ๋•Œ).
62
+ """
63
+ dataset = _dataset(run_meta)
64
+ if dataset is None:
65
+ return "selection unknown (no dataset block)"
66
+ selection = dataset.get("selection") or {}
67
+ parts = []
68
+ where = selection.get("where")
69
+ if where:
70
+ parts.append("where " + " ".join(f"{k}={','.join(v)}" for k, v in sorted(where.items())))
71
+ if selection.get("limit") is not None:
72
+ parts.append(f"limit {selection['limit']}")
73
+ label = " ".join(parts) or "all"
74
+ label = f"{label} ยท {dataset.get('rows_selected')}/{dataset.get('rows_total')} rows"
75
+ sha = dataset.get("sha256")
76
+ if show_sha and isinstance(sha, str):
77
+ label = f"{label} ยท sha {sha[:SHA_DISPLAY_CHARS]}"
78
+ return label
79
+
80
+
81
+ def selections_differ(run_metas: list[dict[str, Any]]) -> bool:
82
+ """๋น„๊ต ๋Œ€์ƒ์˜ ํ–‰ ์ง‘ํ•ฉ์ด ๋‹ค๋ฅธ๊ฐ€ โ€” ์•Œ๋ ค์ง„ ํ‚ค๊ฐ€ ๋‘˜ ์ด์ƒ์ด๊ฑฐ๋‚˜, ๋ชจ๋ฅด๋Š” run ๊ณผ ํ•„ํ„ฐ run ์ด ์„ž์˜€์„ ๋•Œ."""
83
+ signatures = [_selection_signature(m) for m in run_metas]
84
+ known = {s for s in signatures if s is not None}
85
+ if len(known) > 1:
86
+ return True
87
+ unknown = any(s is None for s in signatures)
88
+ return unknown and any(_is_filtered(m) for m in run_metas)
89
+
90
+
91
+ def dataset_bytes_differ(run_metas: list[dict[str, Any]]) -> bool:
92
+ """๊ธฐ๋ก๋œ ๋ฐ์ดํ„ฐ์…‹ sha256 ์ด ๋‘˜ ์ด์ƒ์ธ๊ฐ€ โ€” ํ–‰ ์„ ํƒ๊ณผ ๋ณ„๊ฐœ๋กœ ํŒŒ์ผ ๋‚ด์šฉ์ด ๋ฐ”๋€Œ์—ˆ๋‹ค๋Š” ์‹ ํ˜ธ."""
93
+ shas = {(_dataset(m) or {}).get("sha256") for m in run_metas}
94
+ return len(shas - {None}) > 1
95
+
96
+
97
+ def _collect_run_dirs(
98
+ artifacts_dir: Path, task: str | None, run_ids: str | None, last: int | None
99
+ ) -> list[Path]:
100
+ """๋น„๊ตํ•  run ๋””๋ ‰ํ† ๋ฆฌ โ€” `--runs` ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ๋ชฉ๋ก, ์—†์œผ๋ฉด (ํƒœ์Šคํฌ๋กœ ๊ฑฐ๋ฅธ) ์ „์ฒด."""
101
+ run_dirs: list[Path] = []
102
  if run_ids:
 
 
103
  for rid in run_ids.split(","):
104
  rid = rid.strip()
105
  d = artifacts_dir / rid
 
108
  else:
109
  click.secho(f" Run not found: {rid}, skipping", fg="yellow")
110
  else:
 
 
111
  for d in sorted(artifacts_dir.iterdir()):
112
  if not d.is_dir() or not (d / "run.json").exists():
113
  continue
114
+ if task and json.loads((d / "run.json").read_text()).get("task") != task:
115
+ continue
 
 
116
  run_dirs.append(d)
 
117
  if last and last < len(run_dirs):
118
  run_dirs = run_dirs[-last:]
119
+ return run_dirs
120
 
 
 
 
121
 
122
+ def _load_runs_info(run_dirs: list[Path]) -> list[dict[str, Any]]:
123
+ """ํ‰๊ฐ€๊ฐ€ ์žˆ๋Š” run ๋งŒ (๋ฉ”ํƒ€ + evaluation.json) ์œผ๋กœ ์ฝ๋Š”๋‹ค."""
124
  runs_info = []
 
125
  for d in run_dirs:
 
126
  eval_file = d / "evaluation.json"
127
  if not eval_file.exists():
128
  continue
129
+ run_meta = json.loads((d / "run.json").read_text())
130
  runs_info.append(
131
  {
132
  "run_id": d.name,
 
134
  "model": run_meta.get("model", "-"),
135
  "prompt": run_meta.get("prompt", run_meta.get("prompt_version", "-")),
136
  "pipeline": run_meta.get("pipeline", "-"),
137
+ "eval_data": json.loads(eval_file.read_text()),
138
+ "run_meta": run_meta,
139
  }
140
  )
141
+ return runs_info
142
 
 
 
 
143
 
144
+ def _score_table(runs_info: list[dict[str, Any]]) -> str:
145
+ """์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ ์ˆ˜ + Overall ํ‘œ."""
146
+ all_categories: set[str] = set()
147
+ for r in runs_info:
148
+ all_categories.update(r["eval_data"].get("scores", {}).keys())
149
  headers = ["Category"] + [f"{r['prompt']}/{r['model']}" for r in runs_info]
 
150
  rows = []
151
  for cat in sorted(all_categories):
152
  row = [cat]
 
154
  score = r["eval_data"].get("scores", {}).get(cat)
155
  row.append(f"{score:.4f}" if score is not None else "-")
156
  rows.append(row)
 
 
157
  rows.append(["โ”€" * 20] + ["โ”€" * 8] * len(runs_info))
158
  overall_row = ["Overall"]
159
  for r in runs_info:
160
  overall = r["eval_data"].get("overall_score")
161
  overall_row.append(f"{overall:.4f}" if overall is not None else "-")
162
  rows.append(overall_row)
163
+ return tabulate(rows, headers=headers, tablefmt="simple")
164
 
 
 
165
 
166
+ def _echo_run_details(runs_info: list[dict[str, Any]]) -> None:
167
+ """run ๋ณ„ ํ•œ ์ค„ โ€” ํ–‰ ์„ ํƒ์ด๋‚˜ ๋ฐ์ดํ„ฐ์…‹ ๋ฐ”์ดํŠธ๊ฐ€ ๋‹ค๋ฅด๋ฉด ๊ฐ run ์˜ ์„ ํƒ์„ ํ•จ๊ป˜ ์‹ฃ๋Š”๋‹ค(D19)."""
168
+ metas = [r["run_meta"] for r in runs_info]
169
+ rows_differ = selections_differ(metas)
170
+ bytes_differ = dataset_bytes_differ(metas)
171
  for r in runs_info:
172
+ label = _selection_label(r["run_meta"], show_sha=bytes_differ)
173
+ suffix = f" [{label}]" if rows_differ or bytes_differ else ""
174
+ click.echo(f" {r['prompt']} ({r['model']}, {r['pipeline']}){suffix}")
175
+ if rows_differ:
176
+ click.secho(DIFFERENT_ROWS_WARNING, fg="yellow", bold=True)
177
+ elif bytes_differ:
178
+ click.secho(DIFFERENT_BYTES_WARNING, fg="yellow", bold=True)
179
 
 
180
 
181
+ def _echo_best(runs_info: list[dict[str, Any]]) -> None:
182
  best_run = max(runs_info, key=lambda r: r["eval_data"].get("overall_score", 0))
183
  best_score = best_run["eval_data"].get("overall_score", 0)
184
  click.secho(
 
186
  fg="green",
187
  bold=True,
188
  )
189
+
190
+
191
+ @click.command("compare")
192
+ @click.argument("project")
193
+ @click.option("--task", "-t", default=None, help="Filter by task name")
194
+ @click.option(
195
+ "--runs",
196
+ "run_ids",
197
+ default=None,
198
+ help="Comma-separated run IDs to compare",
199
+ )
200
+ @click.option("--last", "-n", default=None, type=int, help="Compare last N runs for a task")
201
+ @click.pass_context
202
+ def compare_command(ctx, project, task, run_ids, last):
203
+ """Compare evaluation scores across runs.
204
+
205
+ Examples:
206
+ solar-eval compare my-project --task ci-v1-all --last 5
207
+ solar-eval compare my-project --runs "run1,run2"
208
+ """
209
+ cfg = ctx.obj["config"]
210
+ if cfg.is_remote:
211
+ raise click.ClickException("Compare is local-mode only")
212
+
213
+ artifacts_dir = cfg.artifacts_dir(project)
214
+ if not artifacts_dir.exists():
215
+ click.secho("No artifacts found.", fg="yellow")
216
+ return
217
+ run_dirs = _collect_run_dirs(artifacts_dir, task, run_ids, last)
218
+ if not run_dirs:
219
+ click.secho("No runs found to compare.", fg="yellow")
220
+ return
221
+ runs_info = _load_runs_info(run_dirs)
222
+ if not runs_info:
223
+ click.secho("No evaluation data available for comparison.", fg="yellow")
224
+ return
225
+
226
+ click.secho(f"\nComparison: {project}/{task or 'all tasks'}", fg="blue", bold=True)
227
+ _echo_run_details(runs_info)
228
+ click.echo()
229
+ click.echo(_score_table(runs_info))
230
+ _echo_best(runs_info)
solar_eval/cli/commands/failure_keys.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์ถ”๋ก  ์‹คํŒจ ์ƒ˜ํ”Œ์˜ ์ง€๋ฌธ โ€” run.json `failed_sample_keys` (LP5 D-LP5-4 ยท ๊ณ„ํš ยง2.4).
2
+
3
+ ๋Ÿฌ๋„ˆ๋Š” ์‹คํŒจ ์œ„์น˜(`failed_sample_indices`)์™€ ์˜ˆ์™ธ ํƒ€์ž…(`failed_sample_errors`)๋งŒ run.json ์— ์“ด๋‹ค โ€” ๋Ÿฌ๋„ˆ ๊ฒฝ๋กœ๋Š”
4
+ `poc_bundle` ์„ import ํ•˜์ง€ ์•Š๋Š”๋‹ค(D18). ์ด ๋ชจ๋“ˆ์€ CLI ์ธต์ด๋ผ `poc_bundle.keys` ๋กœ ์ง€๋ฌธ์„ ๋œฐ ์ˆ˜ ์žˆ๋‹ค:
5
+ `execute_plan` ์ด ๋Ÿฌ๋„ˆ๊ฐ€ **์ •์ƒ ๋ฐ˜ํ™˜ํ•œ ๋’ค** ์‹คํŒจ ์œ„์น˜๋งˆ๋‹ค ์ค€๋น„๋œ ๋ฐ์ดํ„ฐ์…‹์˜ ์›๋ณธ ํ–‰์— ๊ฒฐ๊ณผ ํ–‰๊ณผ ๊ฐ™์€
6
+ `extract_input` ยท `extract_golden` ์„ ์ ์šฉํ•ด `{sample_idx, input_key, golden_key, error_type}` ๋ฅผ ๋”ํ•œ๋‹ค.
7
+
8
+ ํ…์ŠคํŠธ๋Š” ์‹ฃ์ง€ ์•Š๋Š”๋‹ค โ€” hex ์ง€๋ฌธ๊ณผ ํƒ€์ž… ์ด๋ฆ„๋ฟ์ด๋‹ค. ๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์œผ๋ฉด(์œ„์น˜๊ฐ€ ์„ ํƒ ๋ฐ– ยท ์ž…๋ ฅ์ด ๋นˆ ๊ฐ์ฒด ยท
9
+ JCS ๊ฐ€ ๊ฑฐ๋ถ€ํ•˜๋Š” ๊ฐ’) **์•„๋ฌด๊ฒƒ๋„ ์“ฐ์ง€ ์•Š๋Š”๋‹ค**(= ๋ชจ๋ฆ„). wrap ์€ ๊ทธ๋•Œ `failures.jsonl` ์„ ์ƒ๋žตํ•œ๋‹ค.
10
+ ๋‹จ ์žฌ๊ฐœ ์ „ ์‹œ๋„๊ฐ€ ๋‚จ๊ธด ๋ชฉ๋ก์ด run ๋ฌธ์„œ์— ์žˆ์œผ๋ฉด `[]` ๋กœ ๋ฎ๋Š”๋‹ค(`record_failure_keys` โ€” ๋‚ก์€ ์ง€๋ฌธ์„ ๋‚จ๊ธฐ์ง€ ์•Š๋Š”๋‹ค).
11
+ ๊ฒฝ๊ณ  ํ•œ ์ค„์—๋Š” ์ด์œ ๋งŒ ์ ๊ณ  ๊ฐ’์€ ์ ์ง€ ์•Š๋Š”๋‹ค.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import inspect
18
+ import json
19
+ from collections.abc import Mapping
20
+ from typing import Any
21
+
22
+ import click
23
+
24
+ from poc_bundle.keys import golden_key, input_key
25
+ from poc_bundle.v11 import FAILED_SAMPLE_KEYS_KEY
26
+ from solar_eval.core.run_records import FAILED_SAMPLE_ERRORS_KEY, extract_golden, extract_input
27
+
28
+ #: `failed_sample_errors` ์— ๊ทธ ์œ„์น˜๊ฐ€ ์—†์„ ๋•Œ์˜ ํƒ€์ž… ์ด๋ฆ„(wrap ์€ `exception:unknown` ์œผ๋กœ ์ ๋Š”๋‹ค).
29
+ UNKNOWN_ERROR_TYPE = "unknown"
30
+
31
+
32
+ class _KeysUnknown(Exception):
33
+ """์ง€๋ฌธ์„ ๋œฐ ์ˆ˜ ์—†๋‹ค โ€” ์ด์œ  ํ•œ ๋งˆ๋””๋งŒ ์‹ฃ๋Š”๋‹ค(๊ฐ’์€ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค)."""
34
+
35
+
36
+ def failure_keys(
37
+ run_doc: Mapping[str, Any] | None, prepared: Any, task_config: Mapping[str, Any]
38
+ ) -> list[dict[str, Any]] | None:
39
+ """run ๋ฌธ์„œ์˜ ์‹คํŒจ ์œ„์น˜ โ†’ `failed_sample_keys` ์›์†Œ๋“ค(sample_idx ์ˆœ). ๋ชจ๋ฅด๋ฉด None.
40
+
41
+ `prepared` ๋Š” `PreparedDataset`(๋˜๋Š” `selected` ๊ฐ€ `(์›๋ณธ ์œ„์น˜, ํ–‰)` ๋ชฉ๋ก์ธ ๊ฐ์ฒด)์ด๋‹ค.
42
+ ์‹คํŒจ๊ฐ€ ์—†์œผ๋ฉด `[]` ์ด๋‹ค.
43
+ """
44
+ try:
45
+ indices = _failed_indices(run_doc)
46
+ rows = dict(prepared.selected)
47
+ types = _error_types(run_doc.get(FAILED_SAMPLE_ERRORS_KEY))
48
+ return [_entry(idx, rows, task_config, types) for idx in sorted(indices)]
49
+ except _KeysUnknown as unknown:
50
+ click.secho(f" failure keys not recorded ({unknown})", fg="yellow", err=True)
51
+ return None
52
+
53
+
54
+ def record_failure_keys(
55
+ store: Any, run_id: str, prepared: Any, task_config: Mapping[str, Any]
56
+ ) -> None:
57
+ """run ๋ฌธ์„œ๋ฅผ ์ฝ์–ด ์‹คํŒจ ์ง€๋ฌธ์„ ๊ณ„์‚ฐํ•˜๊ณ  `update_run` ์œผ๋กœ ๋”ํ•œ๋‹ค.
58
+
59
+ run ๋ฌธ์„œ์— ์•ž์„  ์‹œ๋„(์žฌ๊ฐœ ์ „)๊ฐ€ ๋‚จ๊ธด ๋น„์ง€ ์•Š์€ ๋ชฉ๋ก์ด ์žˆ์œผ๋ฉด **๋Š˜ ๋ฎ๋Š”๋‹ค** โ€” ์ €์žฅ์†Œ `update_run` ์€ ๋ณ‘ํ•ฉ์ด๋ผ
60
+ ๋ฎ์ง€ ์•Š์œผ๋ฉด ๋‚ก์€ ๋ชฉ๋ก์ด ๋‚จ๋Š”๋‹ค. ์ƒˆ ๋ชฉ๋ก์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์œผ๋กœ, ์‹คํŒจ๊ฐ€ ์—†์œผ๋ฉด(`[]`, ์žฌ๊ฐœ๊ฐ€ ์‹คํŒจ๋ฅผ ๋ชจ๋‘ ํšŒ๋ณต) `[]` ๋กœ
61
+ (wrap ์ด 0ํ–‰์„ ์“ด๋‹ค), ๋ชจ๋ฅด๋ฉด(None) ์—ญ์‹œ `[]` ๋กœ ๋ฎ๋Š”๋‹ค โ€” `[]` ๋Š” ๋น„์ง€ ์•Š์€ `failed_sample_indices` ์™€ ์ง‘ํ•ฉ์ด ๋‹ฌ๋ผ
62
+ wrap ์ด ์ƒ๋žต(๋ชจ๋ฆ„)ํ•˜๋ฏ€๋กœ, ๋‚ก์€ ์ง€๋ฌธ์ด ์šฐ์—ฐํžˆ ๊ฐ™์€ ์œ„์น˜ ์ง‘ํ•ฉ๊ณผ ๋งž์•„ ํ‹€๋ฆฐ ์Œ์œผ๋กœ ์“ฐ์ด๋Š” ๊ธธ์ด ์—†๋‹ค.
63
+ ๋‚ก์€ ๋ชฉ๋ก์ด ์—†์œผ๋ฉด: ๋ชจ๋ฅด๋ฉด ์“ฐ์ง€ ์•Š๊ณ , ์‹คํŒจ๊ฐ€ ์—†์–ด๋„ ์“ฐ์ง€ ์•Š๋Š”๋‹ค โ€” ์‹คํŒจ ์—†์Œ์€ `failed_sample_indices = []` ๊ฐ€
64
+ ์ด๋ฏธ ๋งํ•œ๋‹ค(์„ฑ๊ณต run ๋งˆ๋‹ค run.json ์„ ํ•œ ๋ฒˆ ๋” ์“ฐ์ง€ ์•Š๋Š”๋‹ค). run ๋ฌธ์„œ๋ฅผ ์ฝ์„ ์ˆ˜ ์—†๋Š” ์ €์žฅ์†Œ(`load_run_meta`
65
+ ์—†์Œ)๋Š” ๋ชจ๋ฆ„์ด๋‹ค.
66
+ """
67
+ loader = getattr(store, "load_run_meta", None)
68
+ run_doc = loader() if callable(loader) else None
69
+ keys = failure_keys(run_doc, prepared, task_config)
70
+ if _has_stale_keys(run_doc):
71
+ keys = keys or []
72
+ elif not keys:
73
+ return
74
+ pending = store.update_run(run_id, {FAILED_SAMPLE_KEYS_KEY: keys})
75
+ if inspect.isawaitable(pending):
76
+ asyncio.run(pending)
77
+
78
+
79
+ def _has_stale_keys(run_doc: Mapping[str, Any] | None) -> bool:
80
+ """run ๋ฌธ์„œ์— `[]` ๊ฐ€ ์•„๋‹Œ `failed_sample_keys` ๊ฐ€ ์ด๋ฏธ ์žˆ๋‹ค(์•ž์„  ์‹œ๋„์˜ ๋ชฉ๋ก โ€” ๋ชจ์–‘์€ ๋”ฐ์ง€์ง€ ์•Š๋Š”๋‹ค)."""
81
+ return isinstance(run_doc, Mapping) and run_doc.get(FAILED_SAMPLE_KEYS_KEY, []) != []
82
+
83
+
84
+ def _failed_indices(run_doc: Mapping[str, Any] | None) -> list[int]:
85
+ if not isinstance(run_doc, Mapping):
86
+ raise _KeysUnknown("run document unreadable")
87
+ indices = run_doc.get("failed_sample_indices")
88
+ if not isinstance(indices, list):
89
+ raise _KeysUnknown("no failed_sample_indices list")
90
+ if not all(isinstance(i, int) and not isinstance(i, bool) and i >= 0 for i in indices):
91
+ raise _KeysUnknown("failed_sample_indices is not a list of positions")
92
+ if len(set(indices)) != len(indices):
93
+ raise _KeysUnknown("failed_sample_indices has duplicates")
94
+ return indices
95
+
96
+
97
+ def _error_types(errors: Any) -> dict[int, str]:
98
+ """`failed_sample_errors` โ†’ `{sample_idx: ํƒ€์ž… ์ด๋ฆ„}`. ๋ชจ์–‘์ด ํ‹€๋ฆฐ ์›์†Œ๋Š” ๊ฑด๋„ˆ๋›ด๋‹ค(โ†’ unknown)."""
99
+ if not isinstance(errors, list):
100
+ return {}
101
+ types: dict[int, str] = {}
102
+ for element in errors:
103
+ if not isinstance(element, Mapping):
104
+ continue
105
+ idx, name = element.get("sample_idx"), element.get("error_type")
106
+ if isinstance(idx, int) and not isinstance(idx, bool) and isinstance(name, str) and name:
107
+ types[idx] = name
108
+ return types
109
+
110
+
111
+ def _entry(
112
+ idx: int, rows: Mapping[int, Any], task_config: Mapping[str, Any], types: Mapping[int, str]
113
+ ) -> dict[str, Any]:
114
+ sample = rows.get(idx)
115
+ if not isinstance(sample, Mapping):
116
+ raise _KeysUnknown("failed position outside the selection")
117
+ input_data = _as_stored(extract_input(sample, task_config.get("input_fields", [])))
118
+ if not input_data:
119
+ raise _KeysUnknown("empty input")
120
+ golden = _as_stored(extract_golden(sample, task_config))
121
+ try:
122
+ # items ๊ทœ์น™๊ณผ ๊ฐ™๋‹ค โ€” ์—”์ง„ run ์˜ ์ž…๋ ฅ ์ •์ฒด์„ฑ์€ ์—”์ง„์ด ๊ณ ๋ฅธ input ๊ฐ์ฒด ์ „์ฒด(fields=None).
123
+ ik, gk = input_key(input_data, None), golden_key(golden)
124
+ except (TypeError, ValueError) as exc: # CanonicalizationError ๋Š” ValueError ๋‹ค
125
+ raise _KeysUnknown(f"input not fingerprintable: {type(exc).__name__}") from None
126
+ return {
127
+ "sample_idx": idx,
128
+ "input_key": ik,
129
+ "golden_key": gk,
130
+ "error_type": types.get(idx, UNKNOWN_ERROR_TYPE),
131
+ }
132
+
133
+
134
+ def _as_stored(value: Any) -> Any:
135
+ """๊ฒฐ๊ณผ ์ €์žฅ์†Œ(`JsonlStore.insert_run_result`)๊ฐ€ ์“ฐ๋Š” ์ง๋ ฌํ™”๋ฅผ ํ•œ ๋ฒˆ ๊ฑฐ์นœ ๊ฐ’.
136
+
137
+ items ๋Š” results.jsonl ์„ ๋‹ค์‹œ ์ฝ์–ด ์ง€๋ฌธ์„ ๋œจ๋ฏ€๋กœ, ๊ฐ™์€ ์›๋ณธ ํ–‰์ด๋ฉด ๊ฐ™์€ ํ‚ค๊ฐ€ ๋‚˜์˜ค๊ฒŒ ๊ฐ™์€
138
+ `json.dumps` ์™•๋ณต์„ ๊ฑฐ์นœ๋‹ค(ํŠœํ”Œ โ†’ ๋ชฉ๋ก ยท ๋ฌธ์ž์—ด์ด ์•„๋‹Œ ํ‚ค โ†’ ๋ฌธ์ž์—ด). ์ง๋ ฌํ™”ํ•  ์ˆ˜ ์—†์œผ๋ฉด ๋ชจ๋ฆ„.
139
+ """
140
+ try:
141
+ return json.loads(json.dumps(value, ensure_ascii=False))
142
+ except (TypeError, ValueError) as exc:
143
+ raise _KeysUnknown(f"input not serializable: {type(exc).__name__}") from None
solar_eval/cli/commands/run_eval.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """`solar-eval runs eval` โ€” ์ƒˆ ์ถ”๋ก  ์—†์ด ๋””์Šคํฌ์˜ ๊ฒฐ๊ณผ๋งŒ ๋‹ค์‹œ ์ฑ„์ ํ•œ๋‹ค (๊ณ„ํš 2026-09-24-phase1a D11).
2
+
3
+ `runs.py` ์—์„œ ๋–ผ์–ด ๋ƒˆ๋‹ค(E1). ์˜ฎ๊ธฐ๋ฉด์„œ ๋ฐ”๋€ ๊ฒƒ ๋‘ ๊ฐ€์ง€:
4
+
5
+ - evaluation.json `eval_model` ์€ ์˜ˆ์ „์˜ `"local"` ์ด ์•„๋‹ˆ๋ผ ํ‰๊ฐ€๊ธฐ๊ฐ€ ๋ณด๊ณ ํ•˜๋Š” judge ๋ชจ๋ธ์ด๋‹ค
6
+ (judge ๊ฐ€ ์—†๋Š” lcs_diffยทrule_based ๋Š” null). ๋Ÿฌ๋„ˆ์™€ ๊ฐ™์€ ๊ทœ์น™์ด๋‹ค.
7
+ - run.json ์— `snapshot` ์ด ์žˆ์œผ๋ฉด `snapshot.config.evaluator_config` ๋ฅผ ์ƒˆ ํ‰๊ฐ€๊ธฐ๋กœ ๋‹ค์‹œ ์“ฐ๊ณ 
8
+ `config_fingerprint` ๋ฅผ ๋‹ค์‹œ ๊ณ„์‚ฐํ•˜๋ฉฐ `provenance.re_evaluated_at` ์„ ๋‚จ๊ธด๋‹ค โ€” ๋ฒˆ๋“ค๋กœ ๊ฐ์Œ€ ๋•Œ
9
+ ํ‰๊ฐ€ ํŒŒ์ผ๊ณผ ์Šค๋ƒ…์ƒท์ด ์–ด๊ธ‹๋‚˜์ง€ ์•Š๊ฒŒ(SPEC ยง4 "ํ‰๊ฐ€ ์ •์ฒด์„ฑ ์—ฐ๊ธฐ"์˜ ์ตœ์†Œ ๋Œ€์‘, Q14).
10
+
11
+ **๋‘ ๊ฒฝ๋กœ(๊ณ„ํš 2026-09-27 L6 ยง2.12 B4 ยท ๋งˆ์Šคํ„ฐ D-M2-6).** hub ๋Œ€์ƒ lcs_diff ๊ณ„์—ด์˜ ์™„๋ฃŒ๋œ run ์€ **run ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฑด๋“œ๋ฆฌ์ง€
12
+ ์•Š๋Š”๋‹ค** โ€” ์ฑ„ํƒ๋œ run ๋ฒˆ๋“ค์˜ ๊ฒฐ๊ณผ๋กœ ์ฑ„์ ํ•ด ํ‰๊ฐ€ ๋””๋ ‰ํ† ๋ฆฌ์— ๋‚จ๊ธฐ๊ณ  `purpose=rescore_after_fix` ์ฑ„์  ๋ฒˆ๋“ค๋กœ ๋ฐœํ–‰ํ•œ ๋’ค
13
+ (opt-in) hub ์— ์‹ฃ๋Š”๋‹ค(`solar_eval.scoring_publish`). ๊ทธ ๋ฐ–(hub ๋Œ€์ƒ ์•„๋‹˜ยท๋น„-lcsยท๋ฏธ์™„๋ฃŒ)์€ ์œ„์˜ ์˜ค๋Š˜ ๋™์ž‘(in-place) +
14
+ ์•ˆ๋‚ด ํ•œ ์ค„์ด๋‹ค. hub ๋Œ€์ƒ run ์„ ์˜› ๋™์ž‘์œผ๋กœ ๋Œ๋ฆฌ๋˜ ํƒˆ์ถœ๊ตฌ ํ”Œ๋ž˜๊ทธ๋Š” L10 ์ปท์˜ค๋ฒ„์—์„œ ์ง€์› ๋‹ค(๊ณ„ํš 2026-09-28 L10 ยง2.6 ยท D-L10-7 โ€” ์ฃผ๋ฉด click ์‚ฌ์šฉ
15
+ ์˜ค๋ฅ˜, ์ข…๋ฃŒ 2). ์ข…๋ฃŒ ์ฝ”๋“œ: ์ฑ„์ ์„ ๋งˆ์ณค์œผ๋ฉด 0(๋ฐœํ–‰ยทhub ์‹คํŒจ์™€ ๋ฌด๊ด€), `--strict-ingest`/
16
+ `SOLAR_EVAL_STRICT_INGEST=1` ์ด๊ณ  ์‹คํŒจ ๋‹ค๋ฆฌ๊ฐ€ ์žˆ์œผ๋ฉด 3, ์ƒˆ ๊ฒฝ๋กœ์—์„œ ๋ฒˆ๋“ค์ด ์—†๊ฑฐ๋‚˜ ์ฑ„์ ์„ ๋ชป ํ–ˆ์œผ๋ฉด 1.
17
+
18
+ **judge ๊ฒฝ๋กœ(๊ณ„ํš 2026-09-28 L12 ยง2.5 ยท D-L12-6ยท9ยท13).** ํƒœ์Šคํฌ ํ‰๊ฐ€๊ธฐ๊ฐ€ `rubric_judge` ์ธ hub ๋Œ€์ƒ ์™„๋ฃŒ run ์€ ๊ธฐ์ค€๋ณ„ judge ์ฑ„์ 
19
+ ๋ฒˆ๋“ค์„ ๋ฐœํ–‰ํ•œ๋‹ค(`scoring_publish.judge`). `--purpose {primary,variance_check}` ์™€ `--retry-of <evaluation_run_id>` ๋Š” ์ด ๊ฒฝ๋กœ์—์„œ๋งŒ
20
+ ๋ฐ›๋Š”๋‹ค(ํ•จ๊ป˜ ์“ฐ๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๊ฒฝ๋กœ์—์„œ ์“ฐ๋ฉด ์‚ฌ์šฉ ์˜ค๋ฅ˜, ์ข…๋ฃŒ 2). judge ์„ค์ •์€ ๊ฒฝ๋กœ ํŒ์ • ์ „์— ํ™•์ธํ•œ๋‹ค.
21
+ judge ๊ฐ€์ง€์˜ ์ข…๋ฃŒ ์ฝ”๋“œ: ๋ฐœํ–‰ ์‚ฌ์ „ ๊ฑฐ๋ถ€ 1 ยท retry ๋Œ€์ƒ ์—†์Œ 0 ยท ์ฑ„์  ๋’ค ์‹คํŒจ๋Š” ์œ„ ๊ทœ์น™ ๊ทธ๋Œ€๋กœ.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ from collections.abc import Mapping
28
+ from datetime import datetime, timezone
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ import click
33
+
34
+ from solar_eval.cli.commands import run_meta as run_meta_module
35
+ from solar_eval.cli.commands.run_meta import PREP_ERRORS, find_task, load_run
36
+ from solar_eval.cli.formatters import format_scores
37
+ from solar_eval.core.run_records import (
38
+ build_eval_detail_docs,
39
+ build_eval_sample_from_result,
40
+ build_evaluation_doc,
41
+ )
42
+ from solar_eval.core.snapshot import self_check, with_evaluator
43
+ from solar_eval.hub_hook import exit_code_for
44
+ from solar_eval.hub_hook.optin import strict_requested
45
+ from solar_eval.hub_hook.types import LEG_PUBLISH, PUBLISH_NO_BUNDLE_PREFIX, STATUS_SKIPPED
46
+ from solar_eval.scoring_publish import FAMILY_JUDGE, EvalJob, Route, decide_route, publish_run_eval
47
+ from solar_eval.scoring_publish import judge as judge_publish
48
+ from solar_eval.scoring_publish.eval_dir import REPORT_SAMPLES
49
+ from solar_eval.scoring_publish.publish import PublishOutcome, Scored
50
+
51
+ #: `--no-ingest`ยท`--strict-ingest` ๋„์›€๋ง(๊ณ„ํš L6 ยง2.2 `runs eval` ํ–‰).
52
+ NO_INGEST_HELP = "์ƒˆ ๊ฒฝ๋กœ์—์„œ hub ๋‹ค๋ฆฌ(์ ์žฌ)๋งŒ ๊ฑด๋„ˆ๋›ด๋‹ค โ€” ํ‰๊ฐ€ ๋””๋ ‰ํ† ๋ฆฌยท์ฑ„์  ๋ฒˆ๋“ค ๋ฐœํ–‰์€ ํ•œ๋‹ค"
53
+ STRICT_INGEST_HELP = (
54
+ "์ƒˆ ๊ฒฝ๋กœ์˜ ๋ฒˆ๋“คยท๋ฐœํ–‰ยทhub ๋‹ค๋ฆฌ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ข…๋ฃŒ ์ฝ”๋“œ 3 (๊ธฐ๋ณธ 0). "
55
+ "SOLAR_EVAL_STRICT_INGEST=1 ๊ณผ ๊ฐ™๋‹ค"
56
+ )
57
+ #: judge ๊ฒฝ๋กœ ์˜ต์…˜ ๋„์›€๋ง(๊ณ„ํš 2026-09-28 L12 D-L12-9).
58
+ PURPOSE_HELP = (
59
+ "rubric judge ์ฑ„์  ๋ฒˆ๋“ค์˜ purpose. ๊ธฐ๋ณธ primary โ€” ๊ฐ™์€ ์„ค์ •์˜ primary ๊ฐ€ ์ด๋ฏธ ์žˆ์œผ๋ฉด ๊ฑฐ๋ถ€ํ•œ๋‹ค. "
60
+ "๊ฐ™์€ ์„ค์ •์„ ๋‹ค์‹œ ์ฑ„์ ํ•ด ํŒ์ • ๋ถ„์‚ฐ์„ ์žฌ๋ ค๋ฉด variance_check"
61
+ )
62
+ RETRY_OF_HELP = "์ด ํ‰๊ฐ€ ์‹คํ–‰(evaluation_run_id, ULID)์˜ error ํŒ์ •๋งŒ ๋‹ค์‹œ ๋ถ€๋ฅธ๋‹ค โ€” purpose=retry"
63
+ #: judge ๊ฒฝ๋กœ์—์„œ๋งŒ ๋ฐ›๋Š” ์˜ต์…˜ โ€” ํ•จ๊ป˜ ์“ฐ๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๊ฒฝ๋กœ์—์„œ ์“ฐ๋ฉด ์‚ฌ์šฉ ์˜ค๋ฅ˜(์ข…๋ฃŒ 2).
64
+ USAGE_PURPOSE_WITH_RETRY = "--purpose ์™€ --retry-of ๋Š” ํ•จ๊ป˜ ์“ธ ์ˆ˜ ์—†๋‹ค"
65
+ USAGE_JUDGE_ONLY = "--purpose/--retry-of ๋Š” rubric judge ์ฑ„์  ๋ฒˆ๋“ค ๊ฒฝ๋กœ์—์„œ๋งŒ ์“ด๋‹ค"
66
+ #: ์ด run ์ด judge ๊ฒฝ๋กœ๊ฐ€ ์•„๋‹Œ ์ด์œ (๊ฒฝ๋กœ ์•ˆ๋‚ด)๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ ์˜ค๋ฅ˜ ๋’ค์— ๋ถ™์ธ๋‹ค.
67
+ USAGE_NOT_JUDGE_REASON = " โ€” ์ด run ์€ judge ๊ฒฝ๋กœ๊ฐ€ ์•„๋‹ˆ๋‹ค: {notice}"
68
+ #: judge ๊ฐ€์ง€์—์„œ ์ฑ„์ ํ•˜์ง€ ๋ชปํ–ˆ์„ ๋•Œ โ€” ์ข…๋ฃŒ 1. ํ”Œ๋ž˜๊ทธ ๊ถŒ์œ ๋Š” ๊ทธ ํ”Œ๋ž˜๊ทธ๊ฐ€ ํ‘ธ๋Š” ์‹คํŒจ์—๋งŒ ๋ถ™์ธ๋‹ค(์•„๋ž˜ ๋‘˜).
69
+ JUDGE_NOT_SCORED = "์ฑ„์ ํ•˜์ง€ ์•Š์•˜๋‹ค โ€” ์œ„ ์š”์•ฝ์˜ ์‹คํŒจ ๋‹ค๋ฆฌ์™€ ๊ทธ ์•ˆ๋‚ด๋ฅผ ๋ณธ๋‹ค"
70
+ #: ๊ฐ™์€ ์„ค์ •์˜ primary ๊ฐ€ ์ด๋ฏธ ์žˆ๋‹ค(`publish_judge:primary_exists`) โ€” purposeยทretry ํ”Œ๋ž˜๊ทธ๊ฐ€ ํ‘ธ๋Š” ์œ ์ผํ•œ ์‹คํŒจ.
71
+ JUDGE_NOT_SCORED_PRIMARY_EXISTS = (
72
+ f"{JUDGE_NOT_SCORED}. ๊ฐ™์€ ์„ค์ •์„ ๋‹ค์‹œ ์ฑ„์ ํ•˜๋ ค๋ฉด --purpose variance_check, "
73
+ "error ๋งŒ ๋‹ค์‹œ ๋ถ€๏ฟฝ๏ฟฝ๋ ค๋ฉด --retry-of <evaluation_run_id>"
74
+ )
75
+ #: ์ฑ„ํƒ๋œ run ๋ฒˆ๋“ค์ด ์—†๋‹ค(`publish_no_bundle:*`) โ€” ํ”Œ๋ž˜๊ทธ๋กœ๋Š” ํ’€๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
76
+ JUDGE_NOT_SCORED_NO_BUNDLE = f"{JUDGE_NOT_SCORED}. ์ฑ„ํƒ๋œ run ๋ฒˆ๋“ค์ด ์—†๋‹ค โ€” ์š”์•ฝ์˜ ๋ฐฑํ•„ ๋ช…๋ น์œผ๋กœ ๋ถ€๋ฅ˜๋ฅผ ํ™•์ธํ•œ ๋’ค ๋‹ค์‹œ ๋Œ๋ฆฐ๋‹ค"
77
+ #: retry ๋Œ€์ƒ์ด ์—†์„ ๋•Œ(์ข…๋ฃŒ 0).
78
+ JUDGE_RETRY_NOTHING = "๋‹ค์‹œ ๋ถ€๋ฅผ error ํŒ์ •์ด ์—†๋‹ค โ€” ์› ํ‰๊ฐ€ ์‹คํ–‰์˜ error ๊ฐ€ ์ด๋ฏธ ๋ชจ๋‘ ํ’€๋ ธ๋‹ค"
79
+
80
+
81
+ def _ulid_option(ctx: click.Context, param: click.Parameter, value: str | None) -> str | None:
82
+ """`--retry-of` ๋Š” ULID ๋ชจ์–‘๋งŒ ๋ฐ›๋Š”๋‹ค โ€” ํ‹€๋ฆฌ๋ฉด ์‚ฌ์šฉ ์˜ค๋ฅ˜(์ข…๋ฃŒ 2)."""
83
+ from poc_bundle.mirror import ULID_RE
84
+
85
+ if value is None or ULID_RE.fullmatch(value) is not None:
86
+ return value
87
+ raise click.BadParameter("evaluation_run_id ๋Š” ULID(26์ž Crockford base32)์—ฌ์•ผ ํ•œ๋‹ค")
88
+
89
+
90
+ async def _score_all(evaluator, judge_provider, results) -> tuple[list[dict], list[dict]]:
91
+ """์ƒ˜ํ”Œ๋ณ„ ์ฑ„์ . ์ฑ„์  ์‹คํŒจ๋Š” ์ ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ ์‹คํŒจ๋กœ ๋‚จ๊ธด๋‹ค โ€” aggregate() ์—๋Š” ์„ฑ๊ณต๋ถ„๋งŒ ๋„˜๊ธด๋‹ค."""
92
+ eval_results: list[dict[str, Any]] = []
93
+ eval_failures: list[dict[str, Any]] = []
94
+ for result in results:
95
+ # ๋ฆฌ์ŠคํŠธ ์œ„์น˜๊ฐ€ ์•„๋‹ˆ๋ผ ์›๋ณธ sample_idx ๊ฐ€ ์ •๋ณธ์ด๋‹ค(F6).
96
+ sample_idx = result["sample_idx"]
97
+ try:
98
+ eval_sample = build_eval_sample_from_result(result, reference=result.get("golden"))
99
+ evaluator.validate_required_fields(eval_sample)
100
+ eval_result = await evaluator.evaluate(sample=eval_sample, provider=judge_provider)
101
+ eval_results.append({**eval_result, "sample_idx": sample_idx})
102
+ except Exception as e:
103
+ click.secho(f" Sample {sample_idx} eval failed: {e}", fg="yellow")
104
+ eval_failures.append({"sample_idx": sample_idx, "error": str(e)})
105
+ return eval_results, eval_failures
106
+
107
+
108
+ def _updated_snapshot(
109
+ run_meta: dict[str, Any], evaluator_config: dict[str, Any] | None, judge_provider: Any
110
+ ) -> dict[str, Any] | None:
111
+ """์Šค๋ƒ…์ƒท์ด ์žˆ์œผ๋ฉด ์ƒˆ ํ‰๊ฐ€๊ธฐ๋กœ ๋‹ค์‹œ ์“ด ์Šค๋ƒ…์ƒท, ์—†์œผ๋ฉด None(์˜› run ์— ์Šค๋ƒ…์ƒท์„ ์ƒˆ๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค).
112
+
113
+ ์ฑ„์  **์ „์—** ๋ถ€๋ฅธ๋‹ค โ€” ๊ธฐ๋ก๋œ ์Šค๋ƒ…์ƒท์ด ๊นจ์กŒ๊ฑฐ๋‚˜ ์ƒˆ ํ‰๊ฐ€๊ธฐ๋กœ ์ง€๋ฌธ์„ ๋‹ค์‹œ ๋œฐ ์ˆ˜ ์—†์œผ๋ฉด ์œ ๋ฃŒ judge
114
+ ํ˜ธ์ถœ์„ ํ•˜๊ธฐ ์ „์— ๋ฉˆ์ถ˜๋‹ค. `judge_provider` ๋Š” ์ด๋ฒˆ ์žฌ์ฑ„์ ์ด ์‹ค์ œ๋กœ ์“ฐ๋Š” ๊ฒƒ์ด๋‹ค โ€” judge ๊ฐ€ ์—†๋˜ run ์„
115
+ llm_judge ๋กœ ๋‹ค์‹œ ์ฑ„์ ํ•˜๋ฉด ๊ทธ ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ์ด `runtime_impl_hash` ์— ๋“ค์–ด๊ฐ€์•ผ ํ•œ๋‹ค(D7).
116
+ `re_evaluated_at` ์€ ์ฑ„์ ์ด ๋๋‚œ ๋’ค `_stamp` ๊ฐ€ ๋ฎ์–ด์“ด๋‹ค.
117
+ """
118
+ snapshot = run_meta.get("snapshot")
119
+ if snapshot is None:
120
+ return None
121
+ started = datetime.now(timezone.utc).isoformat()
122
+ try:
123
+ # ๊ธฐ๋ก๋œ ์Šค๋ƒ…์ƒท๋ถ€ํ„ฐ ๊ฒ€์‚ฌํ•œ๋‹ค โ€” `config` ๊ฐ€ ์—†๋Š” ์Šค๋ƒ…์ƒท์ด ๋งจ KeyError ๋กœ ์ฃฝ์ง€ ์•Š๊ฒŒ.
124
+ self_check(snapshot)
125
+ updated = with_evaluator(
126
+ snapshot, evaluator_config, re_evaluated_at=started, judge_provider=judge_provider
127
+ )
128
+ self_check(updated)
129
+ except PREP_ERRORS as e:
130
+ raise click.ClickException(f"cannot update the run snapshot's evaluator: {e}") from None
131
+ return updated
132
+
133
+
134
+ def _stamp(snapshot: dict[str, Any], at: str) -> dict[str, Any]:
135
+ """`provenance.re_evaluated_at` ์„ ์ฑ„์ ์ด ๋๋‚œ ์‹œ๊ฐ์œผ๋กœ โ€” provenance ๋Š” ์ง€๋ฌธ ๋ฐ–์ด๋‹ค."""
136
+ return {**snapshot, "provenance": {**snapshot["provenance"], "re_evaluated_at": at}}
137
+
138
+
139
+ def _make_evaluator(evaluator_config: dict[str, Any] | None):
140
+ """๋Ÿฌ๋„ˆยท์Šค๋ƒ…์ƒท๊ณผ ๊ฐ™์€ ๊ทœ์น™ โ€” ํ‚ค๊ฐ€ ์—†์„ ๋•Œ๋งŒ ๊ธฐ๋ณธ ํ‰๊ฐ€๊ธฐ. ์ž˜๋ชป๋œ ์„ค์ •์€ ์ฑ„์  ์ „์— ๋ฉˆ์ถ˜๋‹ค."""
141
+ from solar_eval.evaluators.registry import DEFAULT_EVALUATOR_CONFIG, create_evaluator
142
+
143
+ raw = DEFAULT_EVALUATOR_CONFIG if evaluator_config is None else evaluator_config
144
+ try:
145
+ return create_evaluator(raw)
146
+ except ValueError as e:
147
+ raise click.ClickException(f"invalid evaluator config: {e}") from None
148
+
149
+
150
+ async def _evaluate_run(store, run_id, run_meta, task_config, results) -> dict[str, Any]:
151
+ """์ฑ„์  โ†’ evaluation.jsonยทeval_detailsยทrun.json(์ƒํƒœยท์Šค๋ƒ…์ƒท) ์ €์žฅ. ์ง‘๊ณ„๋ฅผ ๋Œ๋ ค์ค€๋‹ค."""
152
+ evaluator_config = task_config.get("evaluator")
153
+ evaluator = _make_evaluator(evaluator_config)
154
+ # judge provider ๋ฅผ ์•ˆ ๋„˜๊ธฐ๋ฉด llm_judge ๊ณ„์—ด์€ ์ „ ์ƒ˜ํ”Œ์—์„œ ์‹คํŒจํ•œ๋‹ค -- ๊ทธ๋ฆฌ๊ณ 
155
+ # ๊ทธ ์‹คํŒจ๊ฐ€ ์˜ˆ์ „์—” 0์  placeholder ๋กœ ๋ฎ์—ฌ ์กฐ์šฉํžˆ completed ๋กœ ๋๋‚ฌ๋‹ค(F8).
156
+ _, judge_provider = run_meta_module.make_providers(evaluator_config)
157
+ snapshot = _updated_snapshot(run_meta, evaluator_config, judge_provider)
158
+ eval_results, eval_failures = await _score_all(evaluator, judge_provider, results)
159
+ now = datetime.now(timezone.utc)
160
+ if snapshot is not None:
161
+ snapshot = _stamp(snapshot, now.isoformat())
162
+
163
+ aggregated = evaluator.aggregate(eval_results)
164
+ # eval_model = ํ‰๊ฐ€๊ธฐ๊ฐ€ ๋ณด๊ณ ํ•˜๋Š” judge ๋ชจ๋ธ(์—†์œผ๏ฟฝ๏ฟฝ None) โ€” ์˜ˆ์ „์˜ "local" ์€ ๊ฑฐ์ง“์ด์—ˆ๋‹ค(D11).
165
+ doc = build_evaluation_doc(
166
+ run_id, aggregated, eval_results, eval_failures, evaluator.judge_model
167
+ )
168
+ eval_id = await store.create_evaluation(doc)
169
+ await store.insert_eval_details(build_eval_detail_docs(eval_id, eval_results, eval_failures))
170
+ update: dict[str, Any] = {"status": "completed", "completed_at": now}
171
+ if snapshot is not None:
172
+ update["snapshot"] = snapshot
173
+ await store.update_run(run_id, update)
174
+ return {**aggregated, "overall_score": doc["overall_score"], "failed": len(eval_failures)}
175
+
176
+
177
+ def _report(aggregated: Mapping[str, Any], run_dir: Path) -> None:
178
+ click.echo()
179
+ click.secho("Evaluation completed!", fg="green", bold=True)
180
+ overall = aggregated.get("overall_score")
181
+ if overall is None:
182
+ # ์ฑ„์ ์ด ํ•œ ๊ฑด๋„ ์„ฑ๊ณตํ•˜์ง€ ๋ชปํ–ˆ๋‹ค -- ์ˆซ์ž๋ฅผ ์ฐ์œผ๋ฉด "0์ "์œผ๋กœ ์ฝํžŒ๋‹ค.
183
+ click.secho(" Overall Score: ์ธก์ • ์—†์Œ (์ „ ์ƒ˜ํ”Œ ์ฑ„์  ์‹คํŒจ)", fg="red", bold=True)
184
+ else:
185
+ click.echo(f" Overall Score: {overall:.4f}")
186
+ samples = aggregated.get(REPORT_SAMPLES)
187
+ if samples is not None:
188
+ click.echo(f" Samples: {samples} (์ฑ„ํƒ๋œ run ๋ฒˆ๋“ค ๊ธฐ์ค€)")
189
+ failed = aggregated.get("failed", 0)
190
+ if failed:
191
+ click.secho(f" ์ฑ„์  ์‹คํŒจ: {failed}๊ฑด (score: null ๋กœ ๊ธฐ๋ก๋จ)", fg="yellow")
192
+ scores = aggregated.get("scores", {})
193
+ if scores:
194
+ click.echo(f" Scores: {format_scores(scores)}")
195
+ click.echo(f"\nResults saved to: {run_dir}/")
196
+
197
+
198
+ @click.command("eval")
199
+ @click.argument("project")
200
+ @click.option("--run-id", "-r", required=True, help="Run ID to evaluate")
201
+ @click.option("--no-ingest", is_flag=True, help=NO_INGEST_HELP)
202
+ @click.option("--strict-ingest", is_flag=True, help=STRICT_INGEST_HELP)
203
+ @click.option(
204
+ "--purpose", type=click.Choice(judge_publish.CLI_PURPOSES), default=None, help=PURPOSE_HELP
205
+ )
206
+ @click.option(
207
+ "--retry-of",
208
+ "retry_of",
209
+ default=None,
210
+ metavar="EVALUATION_RUN_ID",
211
+ callback=_ulid_option,
212
+ help=RETRY_OF_HELP,
213
+ )
214
+ @click.pass_context
215
+ def eval_run(
216
+ ctx: click.Context,
217
+ project: str,
218
+ run_id: str,
219
+ no_ingest: bool,
220
+ strict_ingest: bool,
221
+ purpose: str | None,
222
+ retry_of: str | None,
223
+ ) -> None:
224
+ """Run evaluation only on existing inference results (no new inference)."""
225
+ cfg = ctx.obj["config"]
226
+ if cfg.is_remote:
227
+ raise click.ClickException("Eval is local-mode only")
228
+ if purpose is not None and retry_of is not None:
229
+ raise click.UsageError(USAGE_PURPOSE_WITH_RETRY)
230
+
231
+ run_dir, store, run_meta = load_run(cfg, project, run_id)
232
+ results = store.load_existing_results()
233
+ if not results:
234
+ raise click.ClickException(f"No inference results found in {run_dir}/results.jsonl")
235
+ task = run_meta["task"]
236
+ click.secho(f"Evaluating run: {run_id}", fg="blue", bold=True)
237
+
238
+ _, task_config = find_task(cfg, project, task)
239
+ run_meta_module.preflight_judge_config(task_config)
240
+ route = decide_route(cfg, project, run_dir, run_meta, task_config)
241
+ if route.family != FAMILY_JUDGE and (purpose is not None or retry_of is not None):
242
+ raise click.UsageError(_judge_only_message(route))
243
+ if route.target is None:
244
+ click.secho(f" Task: {task}, Samples: {len(results)}", fg="blue")
245
+ _echo_route(route)
246
+ # ์ด์ „ ํ‰๊ฐ€๋ฅผ ๋ฏธ๋ฆฌ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค(F3) -- ์žฌํ‰๊ฐ€๊ฐ€ ์‹คํŒจํ•ด๋„ ์˜› ์ ์ˆ˜๋Š” ๋‚จ๋Š”๋‹ค.
247
+ aggregated = asyncio.run(_evaluate_run(store, run_id, run_meta, task_config, results))
248
+ _report(aggregated, run_dir)
249
+ return
250
+ # ์ƒˆ ๊ฒฝ๋กœ์˜ ์ฑ„์  ์ž…๋ ฅ์€ ์ฑ„ํƒ๋œ run ๋ฒˆ๋“ค ์‚ฌ๋ณธ์ด๋‹ค โ€” ์›๋ณธ ํ–‰ ์ˆ˜(์ค‘๋ณต ํฌํ•จ)๋Š” ์ฐ์ง€ ์•Š๊ณ  ์ฑ„์  ๋’ค ๋ณด๊ณ ์— ์‹ฃ๋Š”๋‹ค(๋ฆฌ๋ทฐ ์ง€์  4).
251
+ click.secho(f" Task: {task}, Samples: ์ฑ„ํƒ๋œ run ๋ฒˆ๋“ค ๊ธฐ์ค€(์•„๋ž˜ ๋ณด๊ณ )", fg="blue")
252
+ strict = strict_requested(strict_ingest)
253
+ judge_options = (purpose, retry_of)
254
+ scored, code = _publish(route, project, run_dir, task_config, no_ingest, strict, judge_options)
255
+ if scored is not None:
256
+ _report(scored.aggregated, scored.path)
257
+ if code:
258
+ ctx.exit(code)
259
+
260
+
261
+ def _judge_only_message(route: Route) -> str:
262
+ """judge ์ „์šฉ ํ”Œ๋ž˜๊ทธ๋ฅผ ๋‹ค๋ฅธ ๊ฒฝ๋กœ์—์„œ ์“ด ์‚ฌ์šฉ ์˜ค๋ฅ˜ โ€” ๊ฒฝ๋กœ ์•ˆ๋‚ด(๋ฏธ์™„๋ฃŒ statusยท๋Œ€์ƒ ์•„๋‹˜ ๋“ฑ)๊ฐ€ ์žˆ์œผ๋ฉด ์ด์œ ๋กœ ๋ถ™์ธ๋‹ค."""
263
+ if route.notice is None:
264
+ return USAGE_JUDGE_ONLY
265
+ return USAGE_JUDGE_ONLY + USAGE_NOT_JUDGE_REASON.format(notice=route.notice)
266
+
267
+
268
+ def _echo_route(route: Route) -> None:
269
+ """์˜› ๊ฒฝ๋กœ ์•ˆ๋‚ด ํ•œ ์ค„(standalone ์€ ์—†๋‹ค)."""
270
+ if route.notice is not None:
271
+ click.secho(f" โš  {route.notice}", fg="blue")
272
+
273
+
274
+ def _publish(
275
+ route: Route,
276
+ project: str,
277
+ run_dir: Path,
278
+ task_config: dict[str, Any],
279
+ no_ingest: bool,
280
+ strict: bool,
281
+ judge_options: tuple[str | None, str | None] = (None, None),
282
+ ) -> tuple[Scored | None, int]:
283
+ """์ƒˆ ๊ฒฝ๋กœ(`scoring_publish.publish_run_eval`) โ†’ (์ฑ„์ , ์ข…๋ฃŒ ์ฝ”๋“œ). ์ฑ„์ ์„ ๋ชป ํ–ˆ์œผ๋ฉด ClickException(์ข…๋ฃŒ 1).
284
+
285
+ `judge_options` = (`--purpose`, `--retry-of`) โ€” judge ๊ฒฝ๋กœ๋งŒ ์“ด๋‹ค. judge ๊ฐ€์ง€๋Š” `_judge_outcome` ์ด ์ข…๋ฃŒ ์ฝ”๋“œ๋ฅผ ์ •ํ•œ๋‹ค
286
+ (retry ๋Œ€์ƒ ์—†์Œ์ด๋ฉด ์ฑ„์  ์—†์ด ์ข…๋ฃŒ 0 โ€” ๊ทธ๋•Œ ์ฑ„์ ์€ None).
287
+ """
288
+ evaluator_config = task_config.get("evaluator")
289
+ evaluator = _make_evaluator(evaluator_config) # ์ž˜๋ชป๋œ ์„ค์ •์€ ์ž ๊ธˆยท๋ฐฑํ•„ ์ „์— ๋ฉˆ์ถ˜๋‹ค
290
+ _, judge_provider = run_meta_module.make_providers(evaluator_config)
291
+ purpose, retry_of = judge_options
292
+ job = EvalJob(
293
+ project=project,
294
+ run_dir_name=run_dir.name,
295
+ evaluator=evaluator,
296
+ evaluator_config=evaluator_config,
297
+ judge_provider=judge_provider,
298
+ score_all=_score_all,
299
+ no_ingest=no_ingest,
300
+ strict=strict,
301
+ family=route.family,
302
+ purpose=purpose,
303
+ retry_of=retry_of,
304
+ )
305
+ outcome = publish_run_eval(route.target, job)
306
+ if route.family == FAMILY_JUDGE:
307
+ return _judge_outcome(outcome, strict)
308
+ if outcome.scored is None:
309
+ raise click.ClickException(
310
+ "์ฑ„์ ํ•˜์ง€ ์•Š์•˜๋‹ค โ€” ์œ„ ์š”์•ฝ์˜ ์‹คํŒจ ๋‹ค๋ฆฌ์™€ ๋˜์‚ด๋ฆฌ๊ธฐ๋ฅผ ๋ณธ๋‹ค(run ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๊ทธ๋Œ€๋กœ๋‹ค)"
311
+ )
312
+ return outcome.scored, exit_code_for(outcome.result, strict)
313
+
314
+
315
+ def _judge_outcome(outcome: PublishOutcome, strict: bool) -> tuple[Scored | None, int]:
316
+ """judge ๊ฐ€์ง€์˜ ์ข…๋ฃŒ ์ฝ”๋“œ(ยง2.5 ยท ๊ฒ€ํ†  F8). ์ฑ„์ ํ–ˆ์œผ๋ฉด L6b ๊ทœ์น™(0, strict ์ด๊ณ  ์‹คํŒจ ๋‹ค๋ฆฌ๋ฉด 3).
317
+
318
+ ์ฑ„์ ํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฉด: retry ๋Œ€์ƒ ์—†์Œ โ†’ ์•ˆ๋‚ด ํ•œ ์ค„ ๋’ค (None, 0), ๊ทธ ๋ฐ–(๋ฐœํ–‰ ์‚ฌ์ „ ๊ฑฐ๋ถ€ยท๋ฒˆ๋“ค ์—†์Œยท์ž ๊ธˆ ๋“ฑ) โ†’ judge ์ „์šฉ ๋ฌธ๊ตฌ๋กœ
319
+ ์ข…๋ฃŒ 1. purposeยทretry ํ”Œ๋ž˜๊ทธ๋Š” ๊ทธ๊ฒƒ์ด ํ‘ธ๋Š” ์‹คํŒจ(`primary_exists`)์—๋งŒ ๊ถŒํ•œ๋‹ค โ€” ๋ฒˆ๋“ค ์—†์Œยท์ž ๊ธˆ ์‹คํŒจ์—๋Š” ์š”์•ฝ์˜ ์•ˆ๋‚ด๋ฅผ ๊ฐ€๋ฆฌํ‚จ๋‹ค.
320
+ """
321
+ if outcome.scored is not None:
322
+ return outcome.scored, exit_code_for(outcome.result, strict)
323
+ leg = outcome.result.legs.get(LEG_PUBLISH)
324
+ code = leg.code if leg is not None else None
325
+ if leg is not None and (leg.status, code) == (
326
+ STATUS_SKIPPED,
327
+ judge_publish.RETRY_NOTHING_TO_DO,
328
+ ):
329
+ click.secho(f" {JUDGE_RETRY_NOTHING}", fg="blue")
330
+ return None, 0
331
+ raise click.ClickException(_judge_not_scored_message(code))
332
+
333
+
334
+ def _judge_not_scored_message(code: str | None) -> str:
335
+ """๋ฐœํ–‰ ๋‹ค๋ฆฌ ์ฝ”๋“œ โ†’ ์ข…๋ฃŒ 1 ๋ฌธ๊ตฌ. ์ฝ”๋“œ ๋ฌธ์ž์—ด์€ `scoring_publish.judge`ยท`hub_hook.types` ์˜ ์ƒ์ˆ˜๋งŒ ์“ด๋‹ค."""
336
+ if code == f"{judge_publish.PUBLISH_JUDGE_PREFIX}{judge_publish.JUDGE_PRIMARY_EXISTS}":
337
+ return JUDGE_NOT_SCORED_PRIMARY_EXISTS
338
+ if isinstance(code, str) and code.startswith(PUBLISH_NO_BUNDLE_PREFIX):
339
+ return JUDGE_NOT_SCORED_NO_BUNDLE
340
+ return JUDGE_NOT_SCORED
solar_eval/cli/commands/run_meta.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """run ์‹œ์ž‘ยทresume ์ด ๊ณต์œ ํ•˜๋Š” ์ค€๋น„ ๋‹จ๊ณ„์™€ run.json ๋ฉ”ํƒ€ ์กฐ๋ฆฝ (๊ณ„ํš 2026-09-24-phase1a D3ยทD5ยทD12ยทD17ยทD19).
2
+
3
+ `runs start` ๋Š” run ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ธฐ **์ „์—** ์ด ๋ชจ๋“ˆ๋กœ ๋ชจ๋“  ๊ฒƒ์„ ์ •ํ•œ๋‹ค:
4
+ ํƒœ์Šคํฌยทํ”„๋กฌํ”„ํŠธยทํŒŒ์ดํ”„๋ผ์ธ ํ•ด์„ โ†’ ํ–‰ ์„ ํƒ(`where`ยท`limit`) โ†’ ๋ฐ์ดํ„ฐ์…‹ ํ•œ ๋ฒˆ ์ฝ๊ธฐ โ†’ ํŒŒ์ดํ”„๋ผ์ธ ์ƒ์„ฑ โ†’
5
+ ์Šค๋ƒ…์ƒท(run.json `dataset`ยท`snapshot` ๋ธ”๋ก, ์ž๊ธฐ ๊ฒ€์ฆ๊นŒ์ง€). ๊ทธ๋ž˜์„œ 0ํ–‰ ์„ ํƒยท๋ผ๋ฒจ ๋ˆ„๋ฝยท์—†๋Š” ์‚ฌ์ „ยท๊นจ์ง„ lock
6
+ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋นˆ run ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š๊ณ , run.json ์ด ์ ์€ sha256ยท์Šค๋ƒ…์ƒท๊ณผ ๋Ÿฌ๋„ˆ๊ฐ€ ์‹ค์ œ๋กœ ๋„๋Š”
7
+ ๋ฐ์ดํ„ฐยทํŒŒ์ดํ”„๋ผ์ธ์ด **๊ฐ™์€ ๊ฐ์ฒด**์—์„œ ๋‚˜์˜จ๋‹ค.
8
+
9
+ resume ์€ ๊ฐ™์€ ์กฐ๋ฆฝ ํ•จ์ˆ˜(`build_snapshot`)๋กœ ์ง€๊ธˆ ๋””์Šคํฌ ๊ธฐ์ค€ ์Šค๋ƒ…์ƒท์„ ๋‹ค์‹œ ๋–  ์„ค์ • ๋“œ๋ฆฌํ”„ํŠธ๋ฅผ ๋ง‰๋Š”๋‹ค(D17).
10
+
11
+ `poc_bundle` import ๋Š” ์ด ๋ชจ๋“ˆ(๊ณผ `run_resume`ยท`run_eval`)๊ณผ `core/snapshot/**` ์—์„œ๋งŒ ํ•œ๋‹ค(D18).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import subprocess
17
+ from collections.abc import Callable, Mapping, Sequence
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import click
23
+ from poc_bundle.data.errors import DataError
24
+
25
+ from solar_eval.core.dataset_loader import DatasetLoader, LoadedJsonl
26
+ from solar_eval.core.dataset_prep import (
27
+ PreparedDataset,
28
+ build_selection,
29
+ prepare_dataset,
30
+ reject_defaults_where,
31
+ )
32
+ from solar_eval.core.pipeline_factory import build_task_pipeline
33
+ from solar_eval.core.row_filter import RowSelection, parse_cli_where
34
+ from solar_eval.core.snapshot import EngineSnapshotInputs, build_engine_snapshot, self_check
35
+ from solar_eval.core.snapshot import environment
36
+ from solar_eval.core.snapshot.dataset_block import describe_dataset
37
+ from solar_eval.evaluators.judge_config import JudgeConfigError, validate_judge_config
38
+
39
+ #: run ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ์‚ฌ๋žŒ์ด ์ฝ์„ ๋ฉ”์‹œ์ง€๋กœ ๋ฐ”๊ฟ” ๋ฉˆ์ถ”๋Š” ์˜ค๋ฅ˜ โ€” ํ–‰ ์„ ํƒยทmeta_fieldsยทdefaults.where
40
+ #: (ValueError ๊ณ„์—ด), ์—†๋Š” ๋ฐ์ดํ„ฐ์…‹ยท์ง€์‹ ์ž์‚ฐ(FileNotFoundError ๊ณ„์—ด), ๊นจ์ง„ datasets.lock(DataError).
41
+ PREP_ERRORS: tuple[type[Exception], ...] = (ValueError, FileNotFoundError, DataError)
42
+
43
+ #: ์ง€๋ฌธ์„ ์‚ฌ๋žŒ์ด ๋ˆˆ์œผ๋กœ ๋Œ€์กฐํ•  ๋•Œ ๋ณด์—ฌ ์ฃผ๋Š” ์•ž์ž๋ฆฌ ์ˆ˜ โ€” legacy `prompt_sha` ํ˜•์‹๊ณผ ๋งž์ถ˜๋‹ค.
44
+ FINGERPRINT_DISPLAY_CHARS = 12
45
+
46
+
47
+ def resolve_prompt(prompts_dir: Path, prompt_name: str) -> Path:
48
+ """Resolve prompt from prompts/ directory (directory or YAML file).
49
+
50
+ Resolution order:
51
+ 1. prompts/{name}/ (directory TXT format)
52
+ 2. prompts/{name}.yaml (flat YAML layout)
53
+ 3. prompts/_library/{name}.yaml (legacy layout)
54
+ """
55
+ dir_path = prompts_dir / prompt_name
56
+ if dir_path.is_dir():
57
+ return dir_path
58
+ yaml_path = prompts_dir / f"{prompt_name}.yaml"
59
+ if yaml_path.exists():
60
+ return yaml_path
61
+ legacy_path = prompts_dir / "_library" / f"{prompt_name}.yaml"
62
+ if legacy_path.exists():
63
+ return legacy_path
64
+ raise click.ClickException(
65
+ f"Prompt '{prompt_name}' not found. Searched:\n"
66
+ f" {dir_path}/ (directory)\n"
67
+ f" {yaml_path} (YAML)\n"
68
+ f" {legacy_path} (legacy)"
69
+ )
70
+
71
+
72
+ def resolve_pipeline_file(project_dir: Path, pipeline_name: str) -> Path:
73
+ """Resolve pipeline YAML from pipelines/ directory."""
74
+ path = project_dir / "pipelines" / f"{pipeline_name}.yaml"
75
+ if not path.exists():
76
+ raise click.ClickException(f"Pipeline '{pipeline_name}' not found: {path}")
77
+ return path
78
+
79
+
80
+ def compose_pipeline_config(project_dir: Path, pipeline_name: str, overrides: dict) -> dict:
81
+ """ํŒŒ์ดํ”„๋ผ์ธ์„ `extends`/override ๊นŒ์ง€ ํ•ด์„ํ•ด ๋Œ๋ ค์ค€๋‹ค.
82
+
83
+ ์กฐ๋ฆฝ ์‹คํŒจ(์—†๋Š” base, ์—†๋Š” ์Šคํ…์„ ๊ฐ€๋ฆฌํ‚ค๋Š” override ๋“ฑ)๋Š” run ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—
84
+ ์ฃฝ์ธ๋‹ค โ€” ํšจ๊ณผ ์—†๋Š” override ๋กœ ๋Œ์•„๊ฐ„ run ์€ ๊ฒฐ๋ก ์„ ์กฐ์šฉํžˆ ์˜ค์—ผ์‹œํ‚จ๋‹ค.
85
+ """
86
+ from solar_eval.core.pipeline_compose import PipelineCompositionError, compose_pipeline
87
+ from solar_eval.core.project_loader import load_pipeline_file
88
+
89
+ pipelines_dir = resolve_pipeline_file(project_dir, pipeline_name).parent
90
+ try:
91
+ composed = load_pipeline_file(pipelines_dir, pipeline_name)
92
+ if overrides:
93
+ composed = compose_pipeline(composed, overrides=overrides)
94
+ return composed
95
+ except (PipelineCompositionError, FileNotFoundError) as e:
96
+ raise click.ClickException(str(e)) from None
97
+
98
+
99
+ def config_source(cfg, project: str) -> str:
100
+ """run ์˜ config ์ถœ์ฒ˜ โ€” ๋ ˆํฌ ๊ด€๋ฆฌ ํ”„๋กœ์ ํŠธ๋ฉด ๋ ˆํฌ ์ƒ๋Œ€ ๊ฒฝ๋กœ, ์•„๋‹ˆ๋ฉด ๋ฐ์ดํ„ฐ ํ™ˆ ์ ˆ๋Œ€ ๊ฒฝ๋กœ."""
101
+ config_dir = cfg.config_dir(project)
102
+ if cfg.is_repo_managed(project) and cfg.repo_root is not None:
103
+ return str(config_dir.relative_to(cfg.repo_root))
104
+ return str(config_dir)
105
+
106
+
107
+ def git_commit(repo_root: Path | None) -> str | None:
108
+ """์žฌํ˜„์šฉ ๋„์žฅ: ๋ ˆํฌ HEAD ์ปค๋ฐ‹. ์ž‘์—… ํŠธ๋ฆฌ๊ฐ€ ๋”๋Ÿฌ์šฐ๋ฉด '-dirty' ๋ฅผ ๋ถ™์ธ๋‹ค (๋ ˆํฌ ์ „์ฒด โ€” D8 ๋ถˆ๋ณ€)."""
109
+ if repo_root is None:
110
+ return None
111
+ try:
112
+ head = subprocess.run(
113
+ ["git", "-C", str(repo_root), "rev-parse", "--short", "HEAD"],
114
+ capture_output=True,
115
+ text=True,
116
+ check=True,
117
+ ).stdout.strip()
118
+ status = subprocess.run(
119
+ ["git", "-C", str(repo_root), "status", "--porcelain"],
120
+ capture_output=True,
121
+ text=True,
122
+ check=True,
123
+ ).stdout.strip()
124
+ return f"{head}-dirty" if status else head
125
+ except (OSError, subprocess.CalledProcessError):
126
+ return None
127
+
128
+
129
+ def find_task(cfg, project: str, task: str) -> tuple[dict[str, Any], dict[str, Any]]:
130
+ """๋กœ๋“œ๋œ ํ”„๋กœ์ ํŠธ ์„ค์ •๊ณผ ํƒœ์Šคํฌ ์„ค์ •. ์—†์œผ๋ฉด ClickException."""
131
+ from solar_eval.core.project_loader import load_all_project_configs
132
+
133
+ configs = load_all_project_configs(cfg.projects_dir, cfg.config_dirs)
134
+ project_config = next((c for c in configs if c["name"] == project), None)
135
+ if not project_config:
136
+ raise click.ClickException(f"Project not found in {cfg.projects_dir}: {project}")
137
+ task_config = next((t for t in project_config.get("tasks", []) if t["name"] == task), None)
138
+ if not task_config:
139
+ raise click.ClickException(f"Task '{task}' not found in project '{project}'")
140
+ return project_config, task_config
141
+
142
+
143
+ def load_run(cfg, project: str, run_id: str) -> tuple[Path, Any, dict[str, Any]]:
144
+ """(run ๋””๋ ‰ํ† ๋ฆฌ, ์Šคํ† ์–ด, run.json). ์—†์œผ๋ฉด ClickException."""
145
+ from solar_eval.stores import JsonlStore
146
+
147
+ run_dir = cfg.artifacts_dir(project) / run_id
148
+ if not run_dir.exists():
149
+ raise click.ClickException(f"Run directory not found: {run_dir}")
150
+ store = JsonlStore(run_dir)
151
+ run_meta = store.load_run_meta()
152
+ if not run_meta:
153
+ raise click.ClickException(f"No run.json found in {run_dir}")
154
+ if not run_meta.get("task"):
155
+ raise click.ClickException("Cannot determine task from run.json")
156
+ return run_dir, store, run_meta
157
+
158
+
159
+ def load_prompt(
160
+ prompt_path: Path,
161
+ model: str,
162
+ temperature: float,
163
+ max_tokens: int,
164
+ reasoning_effort: str | None,
165
+ ) -> tuple[dict[str, Any], str]:
166
+ """ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ์„ ๋Ÿฌ๋„ˆ๊ฐ€ ๋ฐ›๋Š” dict ๋กœ โ€” (prompt, ํฌ๋งท `multi_step`|`single_step`)."""
167
+ from solar_eval.models.prompt_version import (
168
+ detect_prompt_format,
169
+ load_prompt_messages,
170
+ load_step_prompts,
171
+ )
172
+
173
+ fmt = detect_prompt_format(prompt_path)
174
+ if fmt == "multi_step":
175
+ prompt: dict[str, Any] = {"step_prompts": load_step_prompts(prompt_path)}
176
+ else:
177
+ prompt = {"messages": load_prompt_messages(prompt_path)}
178
+ prompt.update({"model": model, "temperature": temperature, "max_tokens": max_tokens})
179
+ if reasoning_effort:
180
+ prompt["reasoning_effort"] = reasoning_effort
181
+ return prompt, fmt
182
+
183
+
184
+ def make_providers(evaluator_config: Mapping[str, Any] | None = None) -> tuple[Any, Any]:
185
+ """(์ถ”๋ก  ํ”„๋กœ๋ฐ”์ด๋”, judge ํ”„๋กœ๋ฐ”์ด๋”). ํ…Œ์ŠคํŠธ๋Š” ์ด ํ•จ์ˆ˜๋ฅผ ๋ฐ”๊ฟ” ๋ผ์›Œ ๋„คํŠธ์›Œํฌ ์—†์ด ๋ˆ๋‹ค.
186
+
187
+ `evaluator_config` ๋Š” ํƒœ์Šคํฌ์˜ ํ‰๊ฐ€๊ธฐ ์„ค์ •(ํ‚ค๊ฐ€ ์—†์œผ๋ฉด None) โ€” judge ์ธ์Šคํ„ด์Šค๋ฅผ ์„ ์–ธ(`judge_provider`)์œผ๋กœ ๊ณ ๋ฅด๋Š”
188
+ ์ž๋ฆฌ๋‹ค(๊ณ„ํš 2026-09-28 L12 ยง2.8 ยท D-L12-10). ์„ ์–ธ์ด ์—†๊ฑฐ๋‚˜ ํŒ์ • ์—†๋Š” ํ‰๊ฐ€๊ธฐ๋Š” ์˜ค๋Š˜์ฒ˜๋Ÿผ `OpenAIProvider()` โ€”
189
+ run.json ์Šค๋ƒ…์ƒท `provenance.providers.judge` ๋ฐ”์ดํŠธ๊ฐ€ ๋ฐ”๋€Œ์ง€ ์•Š๋Š”๋‹ค. ์ถ”๋ก  ํ”„๋กœ๋ฐ”์ด๋”๋Š” ๋Š˜ `UpstageProvider()`.
190
+
191
+ Raises:
192
+ JudgeConfigError: ๋ชจ๋ฅด๋Š” providerยทํ•ฉ์„ฑ ํ‰๊ฐ€๊ธฐ ์•ˆ์˜ provider ์ถฉ๋Œ.
193
+ """
194
+ from solar_eval.evaluators.judge_config import (
195
+ PROVIDER_OPENAI,
196
+ PROVIDER_UPSTAGE,
197
+ judge_provider_name,
198
+ )
199
+ from solar_eval.providers import UpstageProvider
200
+ from solar_eval.providers.openai_provider import OpenAIProvider
201
+
202
+ judge_classes = {PROVIDER_UPSTAGE: UpstageProvider, PROVIDER_OPENAI: OpenAIProvider}
203
+ name = judge_provider_name(evaluator_config) or PROVIDER_OPENAI
204
+ return UpstageProvider(), judge_classes[name]()
205
+
206
+
207
+ def route_inference_provider(provider: Any, step_models: Sequence[str]) -> Any:
208
+ """์Šคํ… ๋ชจ๋ธ์— ๋ผ์šฐํŠธ ์ ‘๋‘์–ด(`timely:<๋ชจ๋ธ>`)๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด ์ถ”๋ก  ์ œ๊ณต์ž๋ฅผ `RoutedProvider` ๋กœ ๊ฐ์‹ผ๋‹ค.
209
+
210
+ ์—†์œผ๋ฉด ๊ทธ๋Œ€๋กœ ๋Œ๋ ค์ค€๋‹ค โ€” ๋ผ์šฐํŠธ ์—†๋Š” run ์˜ provenance(`providers.inference` ๋ชจ๋“ˆ)๊ฐ€ ์˜ˆ์ „ ๋ฐ”์ดํŠธ ๊ทธ๋Œ€๋กœ์—ฌ์•ผ ํ•œ๋‹ค.
211
+ ์™ธ๋ถ€ ๋ผ์šฐํŠธ๋ฅผ ์จ๋„ ๋˜๋Š”๊ฐ€๋Š” ์—”์ง„์ด ์•„๋‹ˆ๋ผ case ์˜ ๋ฐ์ดํ„ฐ ์ •์ฑ…(๊ณ„์•ฝ)์ด ์ •ํ•œ๋‹ค โ€” 2026-10 judge egress ์ •์ฑ… ํ์ง€(#353)์™€ ๊ฐ™์€ ์ž๋ฆฌ.
212
+ """
213
+ from solar_eval.providers import INFERENCE_ROUTES, RoutedProvider
214
+ from solar_eval.providers.routed import route_prefixes
215
+
216
+ if not route_prefixes(list(step_models), INFERENCE_ROUTES):
217
+ return provider
218
+ return RoutedProvider(provider, {k: cls() for k, cls in INFERENCE_ROUTES.items()})
219
+
220
+
221
+ @dataclass(frozen=True)
222
+ class RunPlan:
223
+ """run ํ•˜๋‚˜๏ฟฝ๏ฟฝ๏ฟฝ ๋ฌด์—‡์œผ๋กœ ๋„๋Š”๊ฐ€ โ€” run ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— ์ „๋ถ€ ์ •ํ•ด์ง„๋‹ค."""
224
+
225
+ project: str
226
+ task: str
227
+ project_config: dict[str, Any]
228
+ task_config: dict[str, Any]
229
+ prompt_name: str
230
+ prompt_path: Path
231
+ prompt_format: str
232
+ prompt: dict[str, Any]
233
+ model: str
234
+ temperature: float
235
+ max_tokens: int
236
+ reasoning_effort: str | None
237
+ pipeline_name: str
238
+ pipeline_config: dict[str, Any]
239
+ pipeline_overrides: dict[str, Any] | None
240
+ max_workers: int
241
+ dataset_loader: DatasetLoader
242
+ prepared: PreparedDataset
243
+ pipeline: Any
244
+ provider: Any
245
+ judge_provider: Any
246
+
247
+ @property
248
+ def selection(self) -> RowSelection | None:
249
+ return self.prepared.selection
250
+
251
+
252
+ def resolve_pipeline(
253
+ cfg, project: str, task_config: dict[str, Any], pipeline_name: str | None, overrides: dict
254
+ ) -> tuple[dict[str, Any], str, dict[str, Any]]:
255
+ """(ํŒŒ์ดํ”„๋ผ์ธ์„ ์–น์€ task_config, ์‹คํšจ ํŒŒ์ดํ”„๋ผ์ธ ์ด๋ฆ„, ํ•ด์„๋œ ํŒŒ์ดํ”„๋ผ์ธ dict).
256
+
257
+ `-P` ๊ฐ€ ์žˆ์œผ๋ฉด config ์ •๋ณธ์˜ pipelines/ ์—์„œ `extends`ยท`--set` ๊นŒ์ง€ ํ•ด์„ํ•˜๊ณ , ์—†์œผ๋ฉด
258
+ project.yaml ๊ธฐ๋ณธ๊ฐ’(project_loader ๊ฐ€ ์ด๋ฏธ ํ•ด์„)์„ ์“ด๋‹ค. config ๋Š” ๋ ˆํฌ ๊ด€๋ฆฌ ํ”„๋กœ์ ํŠธ๋ฉด git
259
+ ์ชฝ์ด ์ •๋ณธ์ด๋‹ค. `extends` ์ƒ์†๊ณผ `--set` override ๋Š” ์—ฌ๊ธฐ์„œ ์ „๋ถ€ ํ•ด์„๋ผ, ์‹คํ–‰ ์—”์ง„์—๋Š” ์™„์„ฑ๋œ
260
+ steps ๋ชฉ๋ก๋งŒ ๋„˜์–ด๊ฐ„๋‹ค.
261
+ """
262
+ if pipeline_name:
263
+ pipeline_config = compose_pipeline_config(cfg.config_dir(project), pipeline_name, overrides)
264
+ return {**task_config, "pipeline_config": pipeline_config}, pipeline_name, pipeline_config
265
+ if overrides:
266
+ raise click.ClickException("--set requires an explicit --pipeline/-P")
267
+ pc = task_config.get("pipeline_config", {})
268
+ effective = pc.get("name", "default") if isinstance(pc, dict) else str(pc)
269
+ return task_config, effective, pc if isinstance(pc, dict) else {}
270
+
271
+
272
+ class _CheckedReader:
273
+ """`prepare_dataset` ์— ๋„˜๊ธฐ๋Š” ์–‡์€ ๋Œ€๋ฆฌ์ž โ€” ํŒŒ์ผ์„ ์ฝ์€ **์งํ›„**, ํ–‰์„ ๊ณ ๋ฅด๊ธฐ **์ „์—** `on_read` ๋ฅผ ๋ถ€๋ฅธ๋‹ค.
274
+
275
+ resume ์˜ ๋ฐ์ดํ„ฐ์…‹ sha ๊ฐ€๋“œ(D17 ์ˆœ์„œ์˜ ์ฒซ์งธ)๊ฐ€ ์„ ํƒยท๋ผ๋ฒจ ๊ฒ€์‚ฌ๋ณด๋‹ค ๋จผ์ € ๊ฑธ๋ฆฌ๊ฒŒ ํ•˜๋ ค๋Š” ๊ฒƒ์ด๋‹ค โ€”
276
+ ๋ฐ์ดํ„ฐ์…‹์ด ๋ฐ”๋€Œ์–ด ์„ ํƒ์ด 0ํ–‰์ด ๋˜๋ฉด ์›์ธ์€ "where ๊ฐ€ 0ํ–‰" ์ด ์•„๋‹ˆ๋ผ "๋ฐ์ดํ„ฐ์…‹์ด ๋ฐ”๋€Œ์—ˆ๋‹ค" ๋‹ค.
277
+ ๋ฐ”์ดํŠธ๋Š” ์—ฌ์ „ํžˆ ํ•œ ๋ฒˆ๋งŒ ์ฝ๋Š”๋‹ค(D16).
278
+ """
279
+
280
+ def __init__(self, loader: DatasetLoader, on_read: Callable[[LoadedJsonl], None]) -> None:
281
+ self._loader = loader
282
+ self._on_read = on_read
283
+
284
+ def read_jsonl(self, repo: str, path: str, source: str) -> LoadedJsonl:
285
+ loaded = self._loader.read_jsonl(repo, path, source=source)
286
+ self._on_read(loaded)
287
+ return loaded
288
+
289
+
290
+ def _step_models(pipeline: Any) -> list[str]:
291
+ """ํŒŒ์ดํ”„๋ผ์ธ ์Šคํ…๋“ค์ด ๋ช…์‹œํ•œ ๋ชจ๋ธ ๋ฌธ์ž์—ด โ€” ๋ผ์šฐํŠธ ์ ‘๋‘์–ด ๊ฒ€์‚ฌ์šฉ(์—†๋Š” ์Šคํ…์€ run ์˜ `-m` ๋ชจ๋ธ = Upstage)."""
292
+ return [
293
+ str(step.config["model"])
294
+ for step in getattr(pipeline, "steps", [])
295
+ if isinstance(getattr(step, "config", None), dict) and step.config.get("model")
296
+ ]
297
+
298
+
299
+ def assemble_plan(
300
+ cfg,
301
+ selection: RowSelection | None,
302
+ *,
303
+ on_read: Callable[[LoadedJsonl], None] | None = None,
304
+ **fields: Any,
305
+ ) -> RunPlan:
306
+ """๋ฐ์ดํ„ฐ์…‹์„ ํ•œ ๋ฒˆ ์ฝ์–ด ๊ณ ๋ฅด๊ณ , ๋Ÿฌ๋„ˆ๊ฐ€ ๋Œ ํŒŒ์ดํ”„๋ผ์ธ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ ๋‹ค(D6ยทD16).
307
+
308
+ `fields` ๋Š” `RunPlan` ์˜ ๋‚˜๋จธ์ง€ ํ•„๋“œ(projectยทtask_configยทprompt ๋“ฑ)๋‹ค. `on_read` ๋Š” ๋ฐ์ดํ„ฐ์…‹์„
309
+ ์ฝ์€ ์งํ›„ยท์„ ํƒ ์ „์— ๋ถˆ๋ฆฐ๋‹ค(resume ์˜ sha ๊ฐ€๋“œ โ€” ๊ฑฐ๊ธฐ์„œ ๋‚œ ClickException ์€ ๊ทธ๋Œ€๋กœ ์˜ฌ๋ผ๊ฐ„๋‹ค).
310
+
311
+ judge ์„ค์ • ์˜ค๋ฅ˜๋Š” ๋ฐ์ดํ„ฐ์…‹ ์ฝ๊ธฐยทํŒŒ์ดํ”„๋ผ์ธยทprovider ์ƒ์„ฑ๋ณด๋‹ค ๋จผ์ € ํ™•์ธํ•œ๋‹ค.
312
+ `runs start`ยท`quick-run`ยท`runs batch`(ํ”„๋กฌํ”„ํŠธ๋งˆ๋‹ค)ยท`runs resume` ์ด ๋ชจ๋‘ ์ด ์ž๋ฆฌ๋ฅผ ์ง€๋‚œ๋‹ค.
313
+
314
+ Raises:
315
+ click.ClickException: judge ์„ค์ •ยท์„ ํƒยทmeta_fieldsยท๋ฐ์ดํ„ฐ์…‹ยทํŒŒ์ดํ”„๋ผ์ธ ์ƒ์„ฑ ์˜ค๋ฅ˜(run ์‹œ์ž‘ ์ „).
316
+ """
317
+ preflight_judge_config(fields["task_config"])
318
+ loader = DatasetLoader(base_dir=cfg.projects_dir)
319
+ reader: Any = loader if on_read is None else _CheckedReader(loader, on_read)
320
+ try:
321
+ prepared = prepare_dataset(
322
+ reader, fields["project_config"], fields["task_config"], selection
323
+ )
324
+ pipeline = build_task_pipeline(
325
+ fields["project_config"], fields["task_config"], fields["prompt"], loader
326
+ )
327
+ except PREP_ERRORS as e:
328
+ raise click.ClickException(str(e)) from None
329
+ provider, judge_provider = make_providers(fields["task_config"].get("evaluator"))
330
+ provider = route_inference_provider(provider, _step_models(pipeline))
331
+ return RunPlan(
332
+ dataset_loader=loader,
333
+ prepared=prepared,
334
+ pipeline=pipeline,
335
+ provider=provider,
336
+ judge_provider=judge_provider,
337
+ **fields,
338
+ )
339
+
340
+
341
+ def preflight_judge_config(task_config: Mapping[str, Any]) -> None:
342
+ """Judge ์„ค์ • ์˜ค๋ฅ˜๋ฅผ CLI ์˜ค๋ฅ˜๋กœ ๋ฐ”๊พธ๋˜ provider ๋Š” ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค."""
343
+ try:
344
+ validate_judge_config(task_config)
345
+ except JudgeConfigError as exc:
346
+ raise click.ClickException(str(exc)) from None
347
+
348
+
349
+ def start_options(
350
+ project_config: dict[str, Any],
351
+ task_config: dict[str, Any],
352
+ set_options: tuple[str, ...],
353
+ where: tuple[str, ...],
354
+ limit: int | None,
355
+ ) -> tuple[dict[str, Any], RowSelection]:
356
+ """(`--set` override, ํ–‰ ์„ ํƒ) โ€” ํ”„๋กฌํ”„ํŠธ์™€ ๋ฌด๊ด€ํ•œ ์‹œ์ž‘ ๊ฒ€์‚ฌ. `runs batch` ๋Š” ๋ฃจํ”„ ์ „์— ํ•œ ๋ฒˆ ๋ถ€๋ฅธ๋‹ค.
357
+
358
+ Raises:
359
+ click.ClickException: `defaults.where`ยทwhere ๋ฌธ๋ฒ•ยท`--set` ๋ฌธ๋ฒ• ์˜ค๋ฅ˜.
360
+ """
361
+ from solar_eval.core.pipeline_compose import PipelineCompositionError, parse_set_option
362
+
363
+ try:
364
+ reject_defaults_where(project_config)
365
+ overrides = parse_set_option(set_options)
366
+ selection = build_selection(task_config, parse_cli_where(where), limit)
367
+ except (PipelineCompositionError, *PREP_ERRORS) as e:
368
+ raise click.ClickException(str(e)) from None
369
+ return overrides, selection
370
+
371
+
372
+ def plan_run(
373
+ cfg,
374
+ *,
375
+ project: str,
376
+ task: str,
377
+ prompt_name: str,
378
+ model: str,
379
+ pipeline_name: str | None,
380
+ reasoning_effort: str | None,
381
+ temperature: float,
382
+ max_tokens: int,
383
+ max_workers: int,
384
+ limit: int | None,
385
+ set_options: tuple[str, ...] = (),
386
+ where: tuple[str, ...] = (),
387
+ ) -> RunPlan:
388
+ """`runs start` ์˜ ์ค€๋น„ ๋‹จ๊ณ„ ์ „๋ถ€ โ€” `reject_defaults_where` โ†’ ์„ ํƒ โ†’ `prepare_dataset` โ†’
389
+ `build_task_pipeline` (๊ณ„ํš ยง9 G2 ์ˆœ์„œ). ๋ชจ๋“  ์˜ค๋ฅ˜๋Š” run ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— ๋‚œ๋‹ค.
390
+ """
391
+ project_config, task_config = find_task(cfg, project, task)
392
+ overrides, selection = start_options(project_config, task_config, set_options, where, limit)
393
+ prompt_path = resolve_prompt(cfg.prompts_dir(project), prompt_name)
394
+ prompt, fmt = load_prompt(prompt_path, model, temperature, max_tokens, reasoning_effort)
395
+ task_config, effective, pipeline_config = resolve_pipeline(
396
+ cfg, project, task_config, pipeline_name, overrides
397
+ )
398
+ return assemble_plan(
399
+ cfg,
400
+ selection,
401
+ project=project,
402
+ task=task,
403
+ project_config=project_config,
404
+ task_config=task_config,
405
+ prompt_name=prompt_name,
406
+ prompt_path=prompt_path,
407
+ prompt_format=fmt,
408
+ prompt=prompt,
409
+ model=model,
410
+ temperature=temperature,
411
+ max_tokens=max_tokens,
412
+ reasoning_effort=reasoning_effort,
413
+ pipeline_name=effective,
414
+ pipeline_config=pipeline_config,
415
+ pipeline_overrides=overrides or None,
416
+ max_workers=max_workers,
417
+ )
418
+
419
+
420
+ def dataset_block(cfg, plan: RunPlan) -> dict[str, Any]:
421
+ """run.json `dataset` ๋ธ”๋ก โ€” ์ฝ์€ ๋ฐ”์ดํŠธ์˜ sha256ยทํ–‰ ์ˆ˜ยท์ •๊ทœํ™”๋œ ์„ ํƒยทlock ๋Œ€์กฐ(S6)."""
422
+ prepared = plan.prepared
423
+ selection = plan.selection or RowSelection()
424
+ try:
425
+ return describe_dataset(
426
+ sha256=prepared.sha256,
427
+ rows_total=prepared.rows_total,
428
+ rows_selected=prepared.rows_selected,
429
+ selection=selection.to_json(),
430
+ meta_fields=prepared.meta_fields,
431
+ task_config=plan.task_config,
432
+ loader_cfg=plan.project_config.get("dataset") or {},
433
+ config_dir=cfg.config_dir(plan.project),
434
+ project_name=plan.project,
435
+ repo_root=environment.engine_repo(),
436
+ )
437
+ except PREP_ERRORS as e:
438
+ raise click.ClickException(f"dataset lock check failed: {e}") from None
439
+
440
+
441
+ def build_snapshot(
442
+ cfg,
443
+ plan: RunPlan,
444
+ dataset: dict[str, Any] | None,
445
+ *,
446
+ source: str | None,
447
+ commit: str | None,
448
+ ) -> dict[str, Any]:
449
+ """run.json `snapshot` ๋ธ”๋ก(engine-snapshot/v1). start ์™€ resume ์ด ๊ฐ™์€ ํ•จ์ˆ˜๋ฅผ ์“ด๋‹ค(D17).
450
+
451
+ Raises:
452
+ click.ClickException: ์„ ์–ธํ•œ ์ž์›์ด ์—†๊ฑฐ๋‚˜ ์Šค๋ƒ…์ƒท์ด ์ž๊ธฐ ๊ฒ€์ฆ(D5)์„ ํ†ต๊ณผํ•˜์ง€ ๋ชปํ•  ๋•Œ.
453
+ """
454
+ inputs = EngineSnapshotInputs(
455
+ model=plan.model,
456
+ temperature=plan.temperature,
457
+ max_tokens=plan.max_tokens,
458
+ reasoning_effort=plan.reasoning_effort,
459
+ pipeline_type=plan.task_config.get("pipeline", "single_step"),
460
+ input_fields=plan.task_config.get("input_fields", []),
461
+ resolved_pipeline=plan.pipeline_config,
462
+ prompt_path=plan.prompt_path,
463
+ pipeline=plan.pipeline,
464
+ evaluator_config=plan.task_config.get("evaluator"),
465
+ inference_provider=plan.provider,
466
+ judge_provider=plan.judge_provider,
467
+ prompt_name=plan.prompt_name,
468
+ pipeline_name=plan.pipeline_name,
469
+ config_source=source,
470
+ git_commit=commit,
471
+ pipeline_overrides=plan.pipeline_overrides,
472
+ max_workers=plan.max_workers,
473
+ config_dir=cfg.config_dir(plan.project),
474
+ repo_managed=cfg.is_repo_managed(plan.project),
475
+ dataset=dataset,
476
+ )
477
+ try:
478
+ snapshot = build_engine_snapshot(inputs)
479
+ self_check(snapshot)
480
+ except PREP_ERRORS as e:
481
+ raise click.ClickException(f"cannot snapshot this run's config: {e}") from None
482
+ return snapshot
483
+
484
+
485
+ def auto_note(note: str | None, plan: RunPlan) -> str | None:
486
+ """`--where` run ์˜ note ์ž๋™ ๊ธฐ์ž…(D19) โ€” Experiment Hub ๋ชฉ๋ก์—์„œ ์ „์ฒด์…‹ ๋ฐ˜๋ณต๊ณผ ์‚ฌ๋žŒ์ด ๊ตฌ๋ถ„ํ•˜๊ฒŒ.
487
+
488
+ note ๊ฐ€ ๋น„๋ฉด `where tags=yul-ryul ยท 12/159 rows`, ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ๋ฉด ๊ทธ ๋’ค์— ` [where โ€ฆ]`.
489
+ where ๊ฐ€ ์—†๋Š” run ์€ note ๋ฅผ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค(๋‹ค๋ฅธ run ์˜ run.json ๋ฐ”์ดํŠธ ๋ถˆ๋ณ€).
490
+ """
491
+ selection = plan.selection
492
+ if selection is None or selection.where is None:
493
+ return note
494
+ prepared = plan.prepared
495
+ label = (
496
+ f"where {selection.where.describe()} ยท {prepared.rows_selected}/{prepared.rows_total} rows"
497
+ )
498
+ return f"{note} [{label}]" if note else label
499
+
500
+
501
+ def build_run_meta(cfg, plan: RunPlan, note: str | None) -> dict[str, Any]:
502
+ """run.json ์ดˆ๊ธฐ ๋‚ด์šฉ โ€” legacy ํ‚ค(๊ฐ’ ๊ทœ์น™ ๋ถˆ๋ณ€, D12) ๋’ค์— `dataset`ยท`snapshot`(๊ณ„ํš ยง3.2 ์ˆœ์„œ)."""
503
+ from solar_eval.core.fingerprint import pipeline_fingerprint, prompt_fingerprint
504
+
505
+ source = config_source(cfg, plan.project)
506
+ commit = git_commit(cfg.repo_root) if cfg.is_repo_managed(plan.project) else None
507
+ dataset = dataset_block(cfg, plan)
508
+ return {
509
+ "task": plan.task,
510
+ "model": plan.model,
511
+ "prompt": plan.prompt_name,
512
+ "pipeline": plan.pipeline_name,
513
+ "temperature": plan.temperature,
514
+ "max_tokens": plan.max_tokens,
515
+ "reasoning_effort": plan.reasoning_effort,
516
+ # ์™œ ๋Œ๋ ธ๋Š”์ง€ โ€” ์˜ˆ์ „์—๋Š” ๋ณ„๋„ ๋žฉ๋…ธํŠธ(experiments/*.yaml)๊ฐ€ ๋‹ด์•˜์ง€๋งŒ run ๊ณผ ๋ถ„๋ฆฌ๋ผ ์žˆ์–ด
517
+ # ์ฐธ์กฐ๊ฐ€ ๋Š์–ด์ง€๊ณ  ์ฉ์—ˆ๋‹ค. run ๊ณผ ๊ฐ™์€ ํŒŒ์ผ์— ๋‘๋ฉด ๋Š์–ด์งˆ ์ˆ˜๊ฐ€ ์—†๋‹ค. where run ์€ ์ž๋™ ๊ธฐ์ž…(D19).
518
+ "note": auto_note(note, plan),
519
+ # provenance: ์ด run ์˜ config(ํ”„๋กฌํ”„ํŠธยทํŒŒ์ดํ”„๋ผ์ธ)๊ฐ€ ์–ด๋””์„œ ์™”๋Š”์ง€ (W&B/DVC ์‹ ๋„์žฅ)
520
+ "config_source": source,
521
+ "git_commit": commit,
522
+ # ๋‚ด์šฉ ์ง€๋ฌธ โ€” ์ด๋ฆ„์ด ์•„๋‹ˆ๋ผ ๋‚ด์šฉ์œผ๋กœ ์„ค์ •์„ ์‹๋ณ„ํ•œ๋‹ค. ์ด๋ฆ„๋งŒ ๋‹ค๋ฅธ ์‚ฌ๋ณธ,
523
+ # `extends` ๋กœ ์กฐ๋ฆฝํ•œ ๊ฒƒ, `--set` ์œผ๋กœ ์ฆ‰์„์—์„œ ๋งŒ๋“  ๊ฒƒ์ด ๋ชจ๋‘ ๊ฐ™์€ ๊ฐ’์ด๋ฉด
524
+ # ๊ฐ™์€ ์ง€๋ฌธ์„ ๊ฐ–๋Š”๋‹ค (core/fingerprint.py). legacy ๊ฐ’ ๊ทœ์น™ ๊ทธ๋Œ€๋กœ(D12) โ€” ์ƒˆ ์ง€๋ฌธ์€ snapshot ์—.
525
+ "prompt_sha": prompt_fingerprint(plan.prompt_path),
526
+ "pipeline_sha": pipeline_fingerprint(plan.pipeline_config),
527
+ # ํŒŒ์ผ ์—†์ด ๋Œ๋ฆฐ ์‹คํ—˜์ด๋ฉด ๋ฌด์—‡์„ ๋ฐ”๊ฟจ๋Š”์ง€๊ฐ€ ์—ฌ๊ธฐ ๋‚จ๋Š”๋‹ค.
528
+ "pipeline_overrides": plan.pipeline_overrides,
529
+ "dataset": dataset,
530
+ "snapshot": build_snapshot(cfg, plan, dataset, source=source, commit=commit),
531
+ }
532
+
533
+
534
+ def short_fingerprint(value: str | None) -> str:
535
+ """64-hex ์ง€๋ฌธ์„ `sha256:<12>` ๋กœ โ€” legacy `prompt_sha` ์™€ ๊ฐ™์€ ๋ชจ์–‘์œผ๋กœ ๋ˆˆ ๋Œ€์กฐ."""
536
+ if not value:
537
+ return "-"
538
+ return f"sha256:{value[:FINGERPRINT_DISPLAY_CHARS]}"
solar_eval/cli/commands/run_progress.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """run ์‹คํ–‰ + ์ง„ํ–‰ ํ‘œ์‹œ (Rich progress bar) โ€” `runs start`ยท`runs resume` ์ด ๊ณต์œ ํ•œ๋‹ค.
2
+
3
+ `runs.py` ๊ฐ€ 800์ค„ ์ƒํ•œ์„ ๋„˜์–ด ๋–ผ์–ด ๋ƒˆ๋‹ค(๊ณ„ํš 2026-09-24-phase1a E1). ๋™์ž‘์€ ์˜ฎ๊ธฐ๊ธฐ ์ „๊ณผ ๊ฐ™๊ณ ,
4
+ ๋Ÿฌ๋„ˆ์— ์„ ํƒยท์ค€๋น„๋œ ๋ฐ์ดํ„ฐ์…‹ยทํŒŒ์ดํ”„๋ผ์ธ ์ธ์Šคํ„ด์Šค๋ฅผ ๋„˜๊ธฐ๋Š” ์ธ์ž๋งŒ ๋”ํ–ˆ๋‹ค(D3ยทD6).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import Any
12
+
13
+ import click
14
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
15
+
16
+ from solar_eval.cli.formatters import format_scores
17
+
18
+ #: ์ง„ํ–‰ ํ‘œ์‹œ ์œ„๋กœ ๋Œ์–ด์˜ฌ๋ ค ์ฐ๋Š” ๋กœ๊ฑฐ๋“ค.
19
+ _LOG_NAMES = (
20
+ "solar_eval.pipelines.pipeline",
21
+ "solar_eval.pipelines.steps.llm",
22
+ "solar_eval.core.runner",
23
+ "solar_eval.providers.upstage",
24
+ )
25
+
26
+
27
+ class _ProgressView:
28
+ """Rich ์ง„ํ–‰ ํ‘œ์‹œ(๊ฐ€๋Šฅํ•  ๋•Œ)์™€ ํ‰๋ฌธ ์ถœ๋ ฅ ์‚ฌ์ด๋ฅผ ๊ฐ€๋ฅด๋Š” ์ž‘์€ ์ƒํƒœ ๋ฌถ์Œ."""
29
+
30
+ def __init__(self) -> None:
31
+ from rich.console import Console
32
+ from rich.live import Live
33
+
34
+ self.bar = Progress(
35
+ SpinnerColumn(),
36
+ TextColumn("[progress.description]{task.description}"),
37
+ BarColumn(),
38
+ TextColumn("{task.completed}/{task.total}"),
39
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
40
+ TimeElapsedColumn(),
41
+ )
42
+ self.live: Live | None = None
43
+ self.use_rich = True
44
+ self.inference_task_id: Any = None
45
+ self.eval_task_id: Any = None
46
+ self._handler: logging.Handler | None = None
47
+ self._closed = False
48
+ try:
49
+ self.live = Live(self.bar, console=Console(stderr=True), refresh_per_second=8)
50
+ self.live.start()
51
+ except Exception as e: # ํ„ฐ๋ฏธ๋„์ด Rich ๋ฅผ ๋ชป ๋ฐ›์œผ๋ฉด ํ‰๋ฌธ์œผ๋กœ โ€” ์›์ธ์€ ํ•œ ์ค„ ๋‚จ๊ธด๋‹ค.
52
+ click.secho(f" (์ง„ํ–‰ ํ‘œ์‹œ ์—†์ด ํ‰๋ฌธ์œผ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค: {e})", fg="yellow")
53
+ self.use_rich = False
54
+ if self.use_rich:
55
+ self._redirect_logs()
56
+
57
+ def _redirect_logs(self) -> None:
58
+ """ํŒŒ์ดํ”„๋ผ์ธยท๋Ÿฌ๋„ˆ ๋กœ๊ทธ๋ฅผ ์ง„ํ–‰ ํ‘œ์‹œ ์œ„๋กœ ์˜ฌ๋ฆฐ๋‹ค."""
59
+ from rich.text import Text
60
+
61
+ live = self.live
62
+
63
+ class _LiveLogHandler(logging.Handler):
64
+ def emit(self, record):
65
+ if live:
66
+ live.console.print(Text(record.getMessage(), style="dim"))
67
+
68
+ self._handler = _LiveLogHandler()
69
+ for name in _LOG_NAMES:
70
+ lg = logging.getLogger(name)
71
+ lg.addHandler(self._handler)
72
+ lg.propagate = False
73
+ logging.getLogger("httpx").setLevel(logging.WARNING)
74
+
75
+ def cleanup(self) -> None:
76
+ """์ง„ํ–‰ ํ‘œ์‹œ๋ฅผ ๋‹ซ๊ณ  ๋กœ๊ฑฐ๋ฅผ ๋˜๋Œ๋ฆฐ๋‹ค. ์—ฌ๋Ÿฌ ๋ฒˆ ๋ถˆ๋Ÿฌ๋„ ๋œ๋‹ค(๋ ์ด๋ฒคํŠธ + finally)."""
77
+ if self.use_rich and self.live is not None and not self._closed:
78
+ self._closed = True
79
+ self.live.stop()
80
+ for name in _LOG_NAMES:
81
+ logging.getLogger(name).removeHandler(self._handler)
82
+ logging.getLogger(name).propagate = True
83
+
84
+
85
+ def _on_inference(view: _ProgressView, etype: str, event: dict[str, Any]) -> None:
86
+ if etype == "dataset_loaded":
87
+ if view.use_rich:
88
+ view.inference_task_id = view.bar.add_task("Inference", total=event["total"])
89
+ else:
90
+ click.secho(f" Dataset loaded: {event['total']} samples", fg="blue")
91
+ elif etype == "inference_progress":
92
+ if view.use_rich and view.inference_task_id is not None:
93
+ view.bar.update(view.inference_task_id, completed=event["completed"])
94
+ elif not view.use_rich:
95
+ c, t = event["completed"], event["total"]
96
+ if c % 10 == 0 or c == t:
97
+ click.echo(f" Inference: {c}/{t}")
98
+ elif etype == "inference_complete":
99
+ if view.use_rich and view.inference_task_id is not None:
100
+ view.bar.update(view.inference_task_id, completed=event["total"])
101
+ else:
102
+ click.secho(f" Inference complete: {event['total']} samples", fg="green")
103
+
104
+
105
+ def _on_evaluation(view: _ProgressView, etype: str, event: dict[str, Any]) -> None:
106
+ if etype == "evaluation_start":
107
+ if view.use_rich:
108
+ view.eval_task_id = view.bar.add_task("Evaluation", total=event.get("total", 0) or 1)
109
+ else:
110
+ click.secho(" Starting evaluation...", fg="blue")
111
+ elif etype == "evaluation_progress" and view.use_rich and view.eval_task_id is not None:
112
+ view.bar.update(view.eval_task_id, completed=event["completed"])
113
+
114
+
115
+ def _on_finish(view: _ProgressView, etype: str, event: dict[str, Any]) -> None:
116
+ if etype == "done":
117
+ if view.use_rich and view.eval_task_id is not None:
118
+ view.bar.update(view.eval_task_id, completed=view.bar.tasks[view.eval_task_id].total)
119
+ view.cleanup()
120
+ click.echo()
121
+ click.secho("Run completed!", fg="green", bold=True)
122
+ overall = event.get("overall_score")
123
+ if overall is not None:
124
+ click.echo(f" Overall Score: {overall:.4f}")
125
+ scores = event.get("scores", {})
126
+ if scores:
127
+ click.echo(f" Scores: {format_scores(scores)}")
128
+ elif etype == "error":
129
+ view.cleanup()
130
+ click.echo()
131
+ click.secho(f" Run failed: {event.get('error')}", fg="red", bold=True)
132
+
133
+
134
+ def _run_with_progress(
135
+ runner,
136
+ run_id,
137
+ project_config,
138
+ task_config,
139
+ prompt,
140
+ max_workers,
141
+ limit,
142
+ completed_results: list[dict[str, Any]] | None = None,
143
+ selection=None,
144
+ prepared=None,
145
+ pipeline=None,
146
+ ):
147
+ """Execute a run with Rich progress bar display.
148
+
149
+ `completed_results` ๋Š” resume ์ด ์ด๋ฏธ ๋๋‚œ ์ƒ˜ํ”Œ์„ ๋‹ค์‹œ ๋Œ๋ฆฌ์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๋‹ค๋ฆฌ๋‹ค --
150
+ ์—ฌ๊ธฐ์„œ ์•ˆ ๋„˜๊ธฐ๋ฉด runner ๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฐ–๊ณ  ์žˆ์–ด๋„ ์†Œ์šฉ์ด ์—†๋‹ค(F2). `selection`ยท`prepared`ยท
151
+ `pipeline` ์€ CLI ๊ฐ€ run ์‹œ์ž‘ ์ „์— ์ •ํ•œ ํ–‰ ์„ ํƒยท๋ฐ์ดํ„ฐ์…‹ยทํŒŒ์ดํ”„๋ผ์ธ ์ธ์Šคํ„ด์Šค๋‹ค(D3ยทD6).
152
+
153
+ `execute_run` ์€ ์น˜๋ช…์  ์‹คํŒจ์—์„œ raise ํ•˜๋ฏ€๋กœ ์ด ํ•จ์ˆ˜๋„ raise ํ•œ๋‹ค --
154
+ ์ผ๋ถ€๋Ÿฌ ๊ฐ์‹ธ์ง€ ์•Š๋Š”๋‹ค. "error" ์ด๋ฒคํŠธ๊ฐ€ ์ด๋ฏธ ์‚ฌ๋žŒ์ด ์ฝ์„ ๋ฉ”์‹œ์ง€๋ฅผ ์ฐ์—ˆ๊ณ ,
155
+ ์—ฌ๊ธฐ์„œ ๋˜ ๊ฐ์‹ธ๋ฉด ๊ฐ™์€ ๋‚ด์šฉ์ด ๋‘ ๋ฒˆ ๋‚˜์˜จ๋‹ค.
156
+ """
157
+ view = _ProgressView()
158
+
159
+ async def on_progress(event):
160
+ etype = event.get("type")
161
+ _on_inference(view, etype, event)
162
+ _on_evaluation(view, etype, event)
163
+ _on_finish(view, etype, event)
164
+
165
+ try:
166
+ asyncio.run(
167
+ runner.execute_run(
168
+ run_id=run_id,
169
+ project_config=project_config,
170
+ task_config=task_config,
171
+ prompt=prompt,
172
+ on_progress=on_progress,
173
+ max_workers=max_workers,
174
+ limit=limit,
175
+ completed_results=completed_results,
176
+ selection=selection,
177
+ prepared=prepared,
178
+ pipeline=pipeline,
179
+ )
180
+ )
181
+ finally:
182
+ # ๋ ์ด๋ฒคํŠธ ์—†์ด ๋๋‚˜๋„(๋Ÿฌ๋„ˆ๊ฐ€ ์ด๋ฒคํŠธ ์ „์— ์ฃฝ์Œ) ํ„ฐ๋ฏธ๋„ยท๋กœ๊ฑฐ๋ฅผ ์›๋ž˜๋Œ€๋กœ ๋Œ๋ฆฐ๋‹ค.
183
+ view.cleanup()
184
+
185
+
186
+ def execute_plan(
187
+ plan, run_id: str, store, completed_results: list[dict[str, Any]] | None = None
188
+ ) -> None:
189
+ """์ค€๋น„๋œ `RunPlan` ์„ ๋Ÿฌ๋„ˆ๋กœ ๋Œ๋ฆฐ๋‹ค โ€” `runs start`ยท`runs resume` ๊ณต์šฉ.
190
+
191
+ ๋Ÿฌ๋„ˆ๋Š” plan ์ด ์ฅ” **๊ฐ™์€ ์ธ์Šคํ„ด์Šค**(์ค€๋น„๋œ ๋ฐ์ดํ„ฐ์…‹ยทํŒŒ์ดํ”„๋ผ์ธยทํ”„๋กœ๋ฐ”์ด๋”)๋กœ ๋ˆ๋‹ค(D6ยทD16).
192
+ `limit` ์€ `prepared.selection` ์•ˆ์— ์žˆ์œผ๋ฏ€๋กœ ๋”ฐ๋กœ ๋„˜๊ธฐ์ง€ ์•Š๋Š”๋‹ค.
193
+ """
194
+ from solar_eval.core.runner import BatchRunner
195
+
196
+ runner = BatchRunner(
197
+ store=store,
198
+ inference_provider=plan.provider,
199
+ judge_provider=plan.judge_provider,
200
+ dataset_loader=plan.dataset_loader,
201
+ )
202
+ _run_with_progress(
203
+ runner,
204
+ run_id,
205
+ plan.project_config,
206
+ plan.task_config,
207
+ plan.prompt,
208
+ plan.max_workers,
209
+ limit=None,
210
+ completed_results=completed_results,
211
+ prepared=plan.prepared,
212
+ pipeline=plan.pipeline,
213
+ )
214
+ # ๋Ÿฌ๋„ˆ๊ฐ€ raise ํ•˜๋ฉด(FAILED) ์—ฌ๊ธฐ ์˜ค์ง€ ์•Š๋Š”๋‹ค. ํ›…์€ ํ˜ธ์ถœ์ž๊ฐ€ ์ด ํ•จ์ˆ˜ **๋’ค์—** ๋ถ€๋ฅธ๋‹ค โ€” ์ˆœ์„œ ๊ทธ๋Œ€๋กœ.
215
+ _record_failure_keys(plan, run_id, store)
216
+
217
+
218
+ def _record_failure_keys(plan, run_id: str, store) -> None:
219
+ """์‹คํŒจ ์ƒ˜ํ”Œ ์ง€๋ฌธ์„ run.json ์— ๋”ํ•œ๋‹ค(LP5 D-LP5-4). ๋ชป ํ•ด๋„ run ์€ ๊ทธ๋Œ€๋กœ ๋๋‚œ๋‹ค.
220
+
221
+ ์‹คํŒจ ์ง€๋ฌธ์€ ์—†์–ด๋„ ๋˜๋Š” ์ •๋ณด๋‹ค โ€” ์—†์œผ๋ฉด wrap ์ด `failures.jsonl` ์„ ์ƒ๋žต(๋ชจ๋ฆ„)ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๋„์šฐ๋ฏธ์˜
222
+ ์˜ˆ์™ธ๋Š” ๊ฒฝ๊ณ  ํ•œ ์ค„(์˜ˆ์™ธ **ํƒ€์ž…๋งŒ** โ€” ๋ฉ”์‹œ์ง€๋Š” ์ž…๋ ฅ ์กฐ๊ฐ์„ ๋‹ด์„ ์ˆ˜ ์žˆ๋‹ค)๋กœ ๋“œ๋Ÿฌ๋‚ด๊ณ  run ์€ ๊ณ„์† ๋๋‚ธ๋‹ค.
223
+ `poc_bundle` ์€ ์—ฌ๊ธฐ์„œ ์ฒ˜์Œ import ํ•œ๋‹ค โ€” CLI ๋ฅผ ๋„์šฐ๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ๋ฒˆ๋“ค ๊ณ„์ธต์„ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค.
224
+ """
225
+ from solar_eval.cli.commands import failure_keys
226
+
227
+ try:
228
+ failure_keys.record_failure_keys(store, run_id, plan.prepared, plan.task_config)
229
+ except Exception as exc: # ์‹คํŒจ ์ง€๋ฌธ์€ ์„ ํƒ ์ •๋ณด โ€” run ์„ ์‹คํŒจ๋กœ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค(๊ณ„ํš ยง2.4)
230
+ click.secho(f" failure keys not recorded ({type(exc).__name__})", fg="yellow")
solar_eval/cli/commands/run_resume.py ADDED
@@ -0,0 +1,298 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """`solar-eval runs resume` โ€” ๋Š๊ธด run ์„ ์ด์–ด ๋Œ๋ฆฐ๋‹ค (๊ณ„ํš 2026-09-24-phase1a D3ยทD17).
2
+
3
+ `runs.py` ์—์„œ ๋–ผ์–ด ๋ƒˆ๋‹ค(E1). resume ์€ **run.json ์ด ๊ธฐ๋กํ•œ ๊ฒƒ**์œผ๋กœ ์ด์–ด ๊ฐ„๋‹ค โ€” ์ง€๊ธˆ์˜ project.yamlยทCLI ์—์„œ
4
+ ์„ ํƒ์„ ๋‹ค์‹œ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. ๋Ÿฌ๋„ˆ ํ˜ธ์ถœ **์ „์—** ๋„ค ๊ฐ€๋“œ๋ฅผ ์ฐจ๋ก€๋กœ ๊ฑด๋‹ค(์‹คํŒจํ•ด๋„ results ๊ฐ€ ๋Š˜์ง€ ์•Š๋Š”๋‹ค):
5
+
6
+ 1. ๋ฐ์ดํ„ฐ์…‹ sha ๊ฐ€๋“œ โ€” ์ฝ์€ ํŒŒ์ผ sha256 ์ด run.json `dataset.sha256` ๊ณผ ๋‹ค๋ฅด๋ฉด ๋ฉˆ์ถ˜๋‹ค. ํŒŒ์ผ์„ ์ฝ์€
7
+ ์งํ›„ยทํ–‰์„ ๊ณ ๋ฅด๊ธฐ ์ „์— ๊ฑด๋‹ค โ€” ๋ฐ”๋€ ๋ฐ์ดํ„ฐ์…‹์ด ์„ ํƒยท๋ผ๋ฒจ ์˜ค๋ฅ˜๋กœ ์ž˜๋ชป ๋ณด๊ณ ๋˜์ง€ ์•Š๊ฒŒ.
8
+ 2. ์„ค์ • ๋“œ๋ฆฌํ”„ํŠธ ๊ฐ€๋“œ โ€” ์‹œ์ž‘ ๋•Œ์™€ ๊ฐ™์€ ์กฐ๋ฆฝ ํ•จ์ˆ˜๋กœ ์ง€๊ธˆ ๋””์Šคํฌ ๊ธฐ์ค€ ์Šค๋ƒ…์ƒท์„ ๋‹ค์‹œ ๋– 
9
+ `config_fingerprint` ๊ฐ€ ๋‹ค๋ฅด๋ฉด ๋ฉˆ์ถ˜๋‹ค. ์šฐํšŒ ํ”Œ๋ž˜๊ทธ๋Š” ์—†๋‹ค โ€” ๋‘ ์„ค์ •์ด ์„ž์ธ results ๊ฐ€ ํ•œ ์Šค๋ƒ…์ƒท ์•„๋ž˜
10
+ ๋ฌถ์ด๋Š” ๊ฒƒ์ด ์ด ๊ฐ€๋“œ๊ฐ€ ๋ง‰๋Š” ๊ฑฐ์ง“ ๊ทธ ์ž์ฒด๋‹ค(๊ฒ€ํ†  F2ยทM4).
11
+ 3. ์„ ํƒ ๋ฐ– idx ๊ฐ€๋“œ โ€” ์ด๋ฏธ ๋๋‚œ `sample_idx` ๊ฐ€ ์„ ํƒ ๋ฐ–์ด๋ฉด ๋ฉˆ์ถ˜๋‹ค.
12
+ 4. ์ •๋‹ต ํ‚ค ๊ฐ€๋“œ โ€” ๋๋‚œ ํ–‰์˜ ์ €์žฅ๋œ `golden`ยท`input` ์ด ์ง€๊ธˆ ํƒœ์Šคํฌ ์„ค์ •(`golden_field`ยท`golden_fields`ยท
13
+ `input_fields`)์œผ๋กœ ๋ฝ‘์€ ๊ฐ’๊ณผ ๋‹ค๋ฅด๋ฉด ๋ฉˆ์ถ˜๋‹ค. `golden_field` ๋Š” ์ง€๋ฌธ์—๋„ sha ์—๋„ ๋“ค์–ด๊ฐ€์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
14
+
15
+ config ๊ฐ€ ๊ฐ™์œผ๋ฉด `snapshot.provenance.resumes` ์— ์ด๋ฒˆ ์—”์ง„ ์ƒํƒœ๋ฅผ ๋ง๋ถ™์ด๊ณ (dirty OR, ํŠธ๋ฆฌ ํ•ด์‹œ๊ฐ€ ๋ฐ”๋€Œ๋ฉด
16
+ `resumed_with_engine_change`) ์ด์–ด ๊ฐ„๋‹ค. `dataset`ยท`snapshot` ๋ธ”๋ก์ด ์—†๋Š” ์˜› run ์€ ์˜ค๋Š˜์ฒ˜๋Ÿผ ์ „์ฒด ํ–‰์œผ๋กœ ์ด์–ด
17
+ ๊ฐ€๊ณ  ๊ฒฝ๊ณ ๋งŒ ๋‚จ๊ธด๋‹ค โ€” ์‹œ์ž‘ ์‹œ์  ์„ค์ •์„ ๋ชจ๋ฅด๋ฏ€๋กœ ์Šค๋ƒ…์ƒท์„ ์ƒˆ๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
18
+
19
+ ๋๋‚˜๋ฉด `runs start` ์™€ ๊ฐ™์€ run ์ข…๋ฃŒ ํ›…(`hub_hook.run_hook`, command=`resume`)์„ ๋ถ€๋ฅธ๋‹ค(๊ณ„ํš 2026-09-27 L6a ยง2.2) โ€”
20
+ `--no-ingest` ๋Š” ํ›… ์ „์ฒด๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ณ , `--strict-ingest`(๋˜๋Š” `SOLAR_EVAL_STRICT_INGEST=1`)๋ฉด ์‹คํŒจ ๋‹ค๋ฆฌ๊ฐ€ ์žˆ์„ ๋•Œ ์ข…๋ฃŒ 3.
21
+ ์Šค๋ชจํฌ ํŒ์ •์€ run.json ์ด ๊ธฐ๋กํ•œ ์„ ํƒ(`dataset.selection.limit`)์œผ๋กœ ํ•œ๋‹ค(D-L6-10).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ from datetime import datetime, timezone
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ import click
32
+
33
+ from solar_eval.cli.commands.run_meta import (
34
+ PREP_ERRORS,
35
+ RunPlan,
36
+ assemble_plan,
37
+ build_snapshot,
38
+ compose_pipeline_config,
39
+ config_source,
40
+ find_task,
41
+ git_commit,
42
+ load_prompt,
43
+ load_run,
44
+ resolve_prompt,
45
+ )
46
+ from solar_eval.cli.commands.run_progress import execute_plan
47
+ from solar_eval.core.dataset_prep import check_resume_indices
48
+ from solar_eval.core.row_filter import RowSelection, format_indices
49
+ from solar_eval.core.run_records import extract_golden, extract_input
50
+ from solar_eval.core.snapshot import config_diff_keys
51
+ from solar_eval.core.snapshot.reproducibility import merge_resume
52
+ from solar_eval.hub_hook import HookOptions, exit_code_for, run_hook
53
+ from solar_eval.hub_hook.optin import strict_requested
54
+ from solar_eval.hub_hook.target import is_smoke_run
55
+
56
+
57
+ def _recorded_selection(run_meta: dict[str, Any]) -> RowSelection | None:
58
+ """run.json ์ด ๊ธฐ๋กํ•œ ์„ ํƒ. `dataset` ๋ธ”๋ก์ด ์—†๋Š” ์˜› run ์€ None(์ „์ฒด โ€” ์˜ค๋Š˜ ๋™์ž‘)."""
59
+ dataset = run_meta.get("dataset")
60
+ if dataset is None:
61
+ return None
62
+ try:
63
+ return RowSelection.from_json(dataset.get("selection") or {"where": None, "limit": None})
64
+ except PREP_ERRORS as e:
65
+ raise click.ClickException(f"run.json dataset.selection is invalid: {e}") from None
66
+
67
+
68
+ def _resume_task_config(run_meta: dict[str, Any], task_config: dict[str, Any]) -> dict[str, Any]:
69
+ """run.json ์ด ๊ธฐ๋กํ•œ `meta_fields` ๋กœ ๋งž์ถ˜๋‹ค โ€” ํ•œ run ์˜ results ํ–‰ ๋ชจ์–‘์ด ์„ž์ด์ง€ ์•Š๊ฒŒ.
70
+
71
+ `dataset` ๋ธ”๋ก์ด ์—†๋Š” ์˜› run ์€ `meta_fields` ๊ฐ€ ์—†๋˜ ์‹œ์ ˆ run ์ด๋ผ meta ๋ฅผ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค.
72
+ """
73
+ dataset = run_meta.get("dataset")
74
+ recorded = dataset.get("meta_fields") if dataset is not None else None
75
+ base = {k: v for k, v in task_config.items() if k != "meta_fields"}
76
+ return {**base, "meta_fields": list(recorded)} if recorded else base
77
+
78
+
79
+ def _resume_prompt(cfg, project: str, run_meta: dict[str, Any]) -> dict[str, Any]:
80
+ """run.json ์˜ ํ”„๋กฌํ”„ํŠธ ์ด๋ฆ„ยท๋ชจ๋ธยทํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋Ÿฌ๋„ˆ prompt ๋ฅผ ๋‹ค์‹œ ๋งŒ๋“ ๋‹ค(ํ˜„์žฌ ํŒŒ์ผ ๊ธฐ์ค€)."""
81
+ prompt_name = run_meta.get("prompt")
82
+ if not prompt_name:
83
+ raise click.ClickException(
84
+ "Cannot determine prompt from run.json. Only runs created with the new CLI can be resumed."
85
+ )
86
+ model = run_meta.get("model", "solar-pro2")
87
+ temperature = run_meta.get("temperature", 0.0)
88
+ max_tokens = run_meta.get("max_tokens", 4000)
89
+ effort = run_meta.get("reasoning_effort")
90
+ prompt_path = resolve_prompt(cfg.prompts_dir(project), prompt_name)
91
+ prompt, fmt = load_prompt(prompt_path, model, temperature, max_tokens, effort)
92
+ return {
93
+ "prompt_name": prompt_name,
94
+ "prompt_path": prompt_path,
95
+ "prompt_format": fmt,
96
+ "prompt": prompt,
97
+ "model": model,
98
+ "temperature": temperature,
99
+ "max_tokens": max_tokens,
100
+ "reasoning_effort": effort,
101
+ }
102
+
103
+
104
+ def _resume_pipeline(
105
+ cfg, project: str, run_meta: dict[str, Any], task_config: dict[str, Any]
106
+ ) -> dict[str, Any]:
107
+ """run.json ์˜ ํŒŒ์ดํ”„๋ผ์ธ ์ด๋ฆ„(+`--set` overrides)์œผ๋กœ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋‹ค์‹œ ํ•ด์„ํ•œ๋‹ค."""
108
+ overrides = run_meta.get("pipeline_overrides") or {}
109
+ pipeline_name = run_meta.get("pipeline") or "default"
110
+ if pipeline_name != "default":
111
+ # ์›๋ž˜ run ์ด `--set` ์œผ๋กœ ๋ฐ”๊ฟ” ๋Œ๋ฆฐ ๊ฒƒ์ด๋ฉด ๊ทธ override ๊นŒ์ง€ ๋ณต์›ํ•ด์•ผ ๊ฐ™์€ ์„ค์ •์ด๋‹ค.
112
+ pipeline_config = compose_pipeline_config(cfg.config_dir(project), pipeline_name, overrides)
113
+ task_config = {**task_config, "pipeline_config": pipeline_config}
114
+ else:
115
+ pc = task_config.get("pipeline_config")
116
+ pipeline_config = pc if isinstance(pc, dict) else {}
117
+ return {
118
+ "task_config": task_config,
119
+ "pipeline_name": pipeline_name,
120
+ "pipeline_config": pipeline_config,
121
+ "pipeline_overrides": overrides or None,
122
+ }
123
+
124
+
125
+ def _plan_resume(
126
+ cfg, project: str, run_meta: dict[str, Any], max_workers: int
127
+ ) -> tuple[RunPlan, RowSelection | None]:
128
+ """run.json ์˜ ๋ชจ๋ธยทํ”„๋กฌํ”„ํŠธยทํŒŒ์ดํ”„๋ผ์ธ(+overrides)ยท์„ ํƒ์œผ๋กœ ์‹คํ–‰ ๊ณ„ํš์„ ๋‹ค์‹œ ๋งŒ๋“ ๋‹ค.
129
+
130
+ ๋ฐ์ดํ„ฐ์…‹ sha ๊ฐ€๋“œ๋Š” ํŒŒ์ผ์„ ์ฝ์€ ์งํ›„ยทํ–‰์„ ๊ณ ๋ฅด๊ธฐ ์ „์— ๊ฑด๋‹ค(`on_read`) โ€” ๋ฐ์ดํ„ฐ์…‹์ด ๋ฐ”๋€Œ์–ด
131
+ ์„ ํƒ์ด 0ํ–‰์ด ๋˜๊ฑฐ๋‚˜ ๋ผ๋ฒจ์ด ๋น ์กŒ์„ ๋•Œ "where ๊ฐ€ ํ‹€๋ ธ๋‹ค" ๊ฐ€ ์•„๋‹ˆ๋ผ ์›์ธ์„ ์•Œ๋ฆฐ๋‹ค(D17 ์ˆœ์„œ).
132
+ """
133
+ project_config, task_config = find_task(cfg, project, run_meta["task"])
134
+ task_config = _resume_task_config(run_meta, task_config)
135
+ selection = _recorded_selection(run_meta)
136
+ plan = assemble_plan(
137
+ cfg,
138
+ selection,
139
+ on_read=lambda loaded: _guard_dataset_sha(loaded.sha256, run_meta),
140
+ project=project,
141
+ task=run_meta["task"],
142
+ project_config=project_config,
143
+ max_workers=max_workers,
144
+ **_resume_prompt(cfg, project, run_meta),
145
+ **_resume_pipeline(cfg, project, run_meta, task_config),
146
+ )
147
+ return plan, selection
148
+
149
+
150
+ def _guard_dataset_sha(current: str, run_meta: dict[str, Any]) -> None:
151
+ recorded = (run_meta.get("dataset") or {}).get("sha256")
152
+ if recorded is not None and recorded != current:
153
+ raise click.ClickException(
154
+ "the dataset changed since the run started "
155
+ f"(sha256 {recorded[:12]}โ€ฆ โ†’ {current[:12]}โ€ฆ) โ€” start a new run"
156
+ )
157
+
158
+
159
+ def _guard_config_drift(cfg, plan: RunPlan, run_meta: dict[str, Any]) -> dict[str, Any] | None:
160
+ """์ง€๊ธˆ ๋””์Šคํฌ ๊ธฐ์ค€ ์Šค๋ƒ…์ƒท์„ ๋‹ค์‹œ ๋–  ์ง€๋ฌธ์„ ๋Œ€์กฐํ•œ๋‹ค. ๊ฐ™์œผ๋ฉด resume ์„ ๋ฐ˜์˜ํ•œ ์ƒˆ ์Šค๋ƒ…์ƒท(D17)."""
161
+ snapshot = run_meta.get("snapshot")
162
+ if snapshot is None:
163
+ click.secho(
164
+ " โš  run.json ์— snapshot ์ด ์—†๋Š” ์˜› run ์ž…๋‹ˆ๋‹ค โ€” ์„ค์ • ๋“œ๋ฆฌํ”„ํŠธ๋ฅผ ๊ฒ€์‚ฌํ•˜์ง€ ๋ชปํ•˜๊ณ  ์ด์–ด ๊ฐ‘๋‹ˆ๋‹ค.",
165
+ fg="yellow",
166
+ )
167
+ return None
168
+ current = build_snapshot(
169
+ cfg,
170
+ plan,
171
+ run_meta.get("dataset"),
172
+ source=config_source(cfg, plan.project),
173
+ commit=git_commit(cfg.repo_root) if cfg.is_repo_managed(plan.project) else None,
174
+ )
175
+ if current["config_fingerprint"] != snapshot.get("config_fingerprint"):
176
+ changed = config_diff_keys(snapshot.get("config") or {}, current["config"])
177
+ raise click.ClickException(
178
+ f"the run's config changed since it started: {', '.join(changed) or '?'} "
179
+ "โ€” start a new run (mixing two configs in one run is not allowed)"
180
+ )
181
+ at = datetime.now(timezone.utc).isoformat()
182
+ return merge_resume(snapshot, current["provenance"]["engine"], at)
183
+
184
+
185
+ def _guard_selection(plan: RunPlan, completed_indices: set[int]) -> None:
186
+ try:
187
+ check_resume_indices(plan.prepared, completed_indices)
188
+ except PREP_ERRORS as e:
189
+ raise click.ClickException(str(e)) from None
190
+
191
+
192
+ def _drifted_rows(plan: RunPlan, completed_results: list[dict[str, Any]]) -> list[int]:
193
+ """์ด๋ฏธ ๋๋‚œ ํ–‰ ์ค‘ ์ €์žฅ๋œ `input`ยท`golden` ์ด ์ง€๊ธˆ ํƒœ์Šคํฌ ์„ค์ •์œผ๋กœ ๋ฝ‘์€ ๊ฐ’๊ณผ ๋‹ค๋ฅธ sample_idx."""
194
+ records = plan.prepared.loaded.records
195
+ input_fields = plan.task_config.get("input_fields", [])
196
+ drifted = set()
197
+ for result in completed_results:
198
+ row = records[result["sample_idx"]]
199
+ if "golden" in result and result["golden"] != extract_golden(row, plan.task_config):
200
+ drifted.add(result["sample_idx"])
201
+ if "input" in result and result["input"] != extract_input(row, input_fields):
202
+ drifted.add(result["sample_idx"])
203
+ return sorted(drifted)
204
+
205
+
206
+ def _guard_answer_key(plan: RunPlan, completed_results: list[dict[str, Any]]) -> None:
207
+ """์ •๋‹ต ํ‚ค(`golden_field`ยท`golden_fields`)ยท์ž…๋ ฅ ํ•„๋“œ๊ฐ€ run ์‹œ์ž‘ ๋’ค ๋ฐ”๋€Œ์—ˆ์œผ๋ฉด ๋ฉˆ์ถ˜๋‹ค(D17 ๋ณด๊ฐ•).
208
+
209
+ `golden_field` ๋Š” ์Šค๋ƒ…์ƒท config ์—๋„ run.json `dataset` ๋ธ”๋ก์—๋„ ์—†์–ด ์ง€๋ฌธยทsha ๊ฐ€๋“œ๋ฅผ ์ง€๋‚˜๊ฐ„๋‹ค โ€”
210
+ ๊ทธ๋Œ€๋กœ ์ด์–ด ๊ฐ€๋ฉด ํ•œ run ์— ๋‘ ์ •๋‹ต ํ‚ค๊ฐ€ ์„ž์ด๊ณ , ๋๋‚œ ํ–‰๊นŒ์ง€ ์ƒˆ ์ •๋‹ต์œผ๋กœ ์ฑ„์ ๋ผ results ์˜ `golden`
211
+ ๊ณผ ์ ์ˆ˜๊ฐ€ ์–ด๊ธ‹๋‚œ๋‹ค. ๊ทธ๋ž˜์„œ ์ €์žฅ๋œ ๊ฐ’๊ณผ ์ง€๊ธˆ ๋ฝ‘์€ ๊ฐ’์„ ํ–‰๋งˆ๋‹ค ์ง์ ‘ ๋Œ€์กฐํ•œ๋‹ค(์˜› run ๋„ ๊ฐ™์€ ๊ทœ์น™).
212
+ ์„ ํƒ ๋ฐ– idx ๊ฐ€๋“œ ๋’ค์— ๋ถ€๋ฅธ๋‹ค โ€” ๋๋‚œ idx ๊ฐ€ ๋ชจ๋‘ ํŒŒ์ผ ์•ˆ์— ์žˆ๋‹ค.
213
+ """
214
+ drifted = _drifted_rows(plan, completed_results)
215
+ if drifted:
216
+ raise click.ClickException(
217
+ "the task's answer key or input fields changed since the run started "
218
+ f"(stored golden/input differs for {format_indices(drifted)}) โ€” start a new run"
219
+ )
220
+
221
+
222
+ @click.command("resume")
223
+ @click.argument("project")
224
+ @click.option("--run-id", "-r", required=True, help="Run ID to resume")
225
+ @click.option("--max-workers", "-w", default=5, type=int, help="Max parallel workers")
226
+ @click.option(
227
+ "--no-ingest",
228
+ is_flag=True,
229
+ help="run ์ด ๋๋‚œ ๋’ค hub ํ›…(๋ฒˆ๋“คยทhub ์ ์žฌ)์„ ํ†ต์งธ๋กœ ๊ฑด๋„ˆ๋›ด๋‹ค โ€” ๋˜์‚ด๋ฆฌ๋Š” ๋ช…๋ น์„ ํ•œ ์ค„๋กœ ์•Œ๋ ค ์ค€๋‹ค",
230
+ )
231
+ @click.option(
232
+ "--strict-ingest",
233
+ is_flag=True,
234
+ help="hub ํ›… ๋‹ค๋ฆฌ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ข…๋ฃŒ ์ฝ”๋“œ 3 (๊ธฐ๋ณธ 0). SOLAR_EVAL_STRICT_INGEST=1 ๊ณผ ๊ฐ™๋‹ค",
235
+ )
236
+ @click.pass_context
237
+ def resume_run(
238
+ ctx: click.Context,
239
+ project: str,
240
+ run_id: str,
241
+ max_workers: int,
242
+ no_ingest: bool,
243
+ strict_ingest: bool,
244
+ ) -> None:
245
+ """Resume an interrupted evaluation run."""
246
+ cfg = ctx.obj["config"]
247
+ if cfg.is_remote:
248
+ raise click.ClickException("Resume is local-mode only")
249
+ run_dir, store, run_meta = load_run(cfg, project, run_id)
250
+ if run_meta.get("status", "unknown") == "completed":
251
+ raise click.ClickException(f"Run {run_id} is already completed. Nothing to resume.")
252
+
253
+ existing_results = store.load_existing_results()
254
+ completed = {r["sample_idx"] for r in existing_results}
255
+ click.secho(f"Resuming run: {run_id}", fg="blue", bold=True)
256
+ click.secho(
257
+ f" Task: {run_meta['task']}, Already completed: {len(completed)} samples", fg="blue"
258
+ )
259
+
260
+ plan, selection = _plan_resume(cfg, project, run_meta, max_workers)
261
+ if selection is None:
262
+ click.secho(
263
+ " โš  run.json ์— dataset ๋ธ”๋ก์ด ์—†๋Š” ์˜› run โ€” ์ „์ฒด ํ–‰์œผ๋กœ ์ด์–ด ๊ฐ‘๋‹ˆ๋‹ค.", fg="yellow"
264
+ )
265
+ # ์ˆœ์„œ: ๋ฐ์ดํ„ฐ์…‹ sha(์ฝ๋Š” ์ฆ‰์‹œ, _plan_resume ์•ˆ) โ†’ ์„ค์ • ๋“œ๋ฆฌํ”„ํŠธ โ†’ ์„ ํƒ ๋ฐ– idx โ†’ ์ •๋‹ต ํ‚ค (D17).
266
+ # ๋ชจ๋‘ ๋Ÿฌ๋„ˆ ํ˜ธ์ถœ ์ „์ด๋ผ ์‹คํŒจํ•ด๋„ results ๊ฐ€ ๋Š˜์ง€ ์•Š๋Š”๋‹ค.
267
+ merged = _guard_config_drift(cfg, plan, run_meta)
268
+ _guard_selection(plan, completed)
269
+ _guard_answer_key(plan, existing_results)
270
+ if merged is not None:
271
+ asyncio.run(store.update_run(run_id, {"snapshot": merged}))
272
+ click.secho(f" Model: {plan.model}", fg="blue")
273
+
274
+ # ์ด์ „ ํ‰๊ฐ€๋ฅผ ๋ฏธ๋ฆฌ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค(F3) -- create_evaluation() ์ด ์›์ž์  ๊ต์ฒด๋ผ ์•ˆ์ „ํ•˜๋‹ค.
275
+ # ์ด๋ฏธ ๋๋‚œ ์ƒ˜ํ”Œ์€ ๊ฑด๋„ˆ๋›ด๋‹ค -- completed_results ๋ฅผ ์•ˆ ๋„˜๊ธฐ๋ฉด resume ์ด ์ „ ์ƒ˜ํ”Œ์„ ๋‹ค์‹œ ์œ ๋ฃŒ
276
+ # ํ˜ธ์ถœํ•˜๊ณ  results.jsonl ์— ๊ฐ™์€ sample_idx ๋ฅผ ์ค‘๋ณต append ํ•œ๋‹ค(F2).
277
+ execute_plan(plan, run_id, store, completed_results=existing_results)
278
+ click.echo(f"\nResults saved to: {run_dir}/")
279
+ _finish_with_hook(ctx, project, run_dir, run_meta, no_ingest, strict_ingest)
280
+
281
+
282
+ def _finish_with_hook(
283
+ ctx: click.Context,
284
+ project: str,
285
+ run_dir: Path,
286
+ run_meta: dict[str, Any],
287
+ no_ingest: bool,
288
+ strict_ingest: bool,
289
+ ) -> None:
290
+ """run ์ข…๋ฃŒ ํ›…(command=`resume`). ์Šค๋ชจํฌ๋Š” run.json ์ด ๊ธฐ๋กํ•œ ์„ ํƒ์œผ๋กœ ๋ณธ๋‹ค(D-L6-10)."""
291
+ strict = strict_requested(strict_ingest)
292
+ options = HookOptions(
293
+ command="resume", no_ingest=no_ingest, strict=strict, smoke=is_smoke_run(run_meta)
294
+ )
295
+ # ํ›… ์‹คํŒจ๋Š” ๊ธฐ๋ณธ ์ข…๋ฃŒ 0 โ€” strict ์—์„œ๋งŒ 3(D-L6-11 ยท ๋งˆ์Šคํ„ฐ D-M2-3).
296
+ code = exit_code_for(run_hook(ctx.obj["config"], project, run_dir, options), strict)
297
+ if code:
298
+ ctx.exit(code)
solar_eval/cli/commands/runs.py CHANGED
@@ -1,23 +1,41 @@
1
- """Run management commands โ€” local execution."""
 
 
 
 
 
 
2
 
3
- import asyncio
4
  import json
5
- import subprocess
6
  import time
 
7
  from datetime import datetime
8
  from pathlib import Path
9
  from typing import Any
10
 
11
  import click
12
- from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeElapsedColumn
13
 
14
  from solar_eval.cli.commands.projects import _resolve_project_id
15
- from solar_eval.cli.formatters import format_runs_table, format_results_table, format_scores
16
-
17
-
18
- #: run ์ด ๋๋‚œ ๋’ค ์ž๋™ ์ ์žฌ์— ํ—ˆ์šฉํ•˜๋Š” ์‹œ๊ฐ„. ์ „์ฒด ์•„์นด์ด๋ธŒ๊ฐ€ ์•„๋‹ˆ๋ผ ํ”„๋กœ์ ํŠธ ํ•˜๋‚˜๋งŒ
19
- #: ์ ์žฌํ•˜๋ฏ€๋กœ ๋ณดํ†ต ์ˆ˜ ์ดˆ๋ฉด ๋๋‚œ๋‹ค โ€” ๋„˜์–ด๊ฐ€๋ฉด ๋ญ”๊ฐ€ ์ž˜๋ชป๋œ ๊ฒƒ์ด๋ผ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
20
- INGEST_TIMEOUT_SECONDS = 180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  @click.group("runs")
@@ -25,6 +43,31 @@ def runs_group() -> None:
25
  """Manage evaluation runs."""
26
 
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  @runs_group.command("list")
29
  @click.argument("project")
30
  @click.option("--task", "-t", default=None, help="Filter by task name")
@@ -102,7 +145,7 @@ def list_runs(ctx: click.Context, project: str, task: str | None) -> None:
102
  @click.option(
103
  "--note",
104
  default=None,
105
- help="์ด run ์„ ์™œ ๋Œ๋ฆฌ๋Š”์ง€ ํ•œ ์ค„ (run.json ์— ๊ธฐ๋ก๋˜๊ณ  evalhub ๊ฐ€ ๊ทธ๋Œ€๋กœ ์ฝ๋Š”๋‹ค)",
106
  )
107
  @click.option(
108
  "--set",
@@ -114,28 +157,11 @@ def list_runs(ctx: click.Context, project: str, task: str | None) -> None:
114
  "์˜ˆ: --set steps.basic_correction.reasoning_effort=high. ์—ฌ๋Ÿฌ ๋ฒˆ ์“ธ ์ˆ˜ ์žˆ๋‹ค."
115
  ),
116
  )
117
- @click.option(
118
- "--no-ingest",
119
- is_flag=True,
120
- help="run ์ด ๋๋‚œ ๋’ค evalhub ์ž๋™ ์ ์žฌ๋ฅผ ๊ฑด๋„ˆ๋›ด๋‹ค (์—ฌ๋Ÿฌ run ์„ ๋Œ๋ฆฌ๊ณ  ํ•œ ๋ฒˆ์— ์ ์žฌํ•  ๋•Œ)",
121
- )
122
  @click.pass_context
123
- def start_run(
124
- ctx: click.Context,
125
- project: str,
126
- task: str,
127
- prompt_name: str,
128
- model: str,
129
- pipeline_name: str | None,
130
- reasoning_effort: str | None,
131
- temperature: float,
132
- max_tokens: int,
133
- max_workers: int,
134
- limit: int | None,
135
- note: str | None,
136
- set_options: tuple[str, ...],
137
- no_ingest: bool,
138
- ) -> None:
139
  """Start an evaluation run.
140
 
141
  Example:
@@ -147,80 +173,17 @@ def start_run(
147
  -p prompt_dev_v23 -m solar-pro3 -P pipeline_dev_v24 \\
148
  --set steps.basic_correction.repeat_temperatures='[0.0, 0.5]' \\
149
  --note "SC ์˜จ๋„ ํญ ์Šค์œ•"
150
- """
151
- cfg = ctx.obj["config"]
152
-
153
- _start_local(
154
- cfg,
155
- project=project,
156
- task=task,
157
- prompt_name=prompt_name,
158
- model=model,
159
- pipeline_name=pipeline_name,
160
- reasoning_effort=reasoning_effort,
161
- temperature=temperature,
162
- max_tokens=max_tokens,
163
- max_workers=max_workers,
164
- limit=limit,
165
- note=note,
166
- set_options=set_options,
167
- no_ingest=no_ingest,
168
- )
169
-
170
-
171
- def _resolve_prompt(prompts_dir: Path, prompt_name: str) -> Path:
172
- """Resolve prompt from prompts/ directory (directory or YAML file).
173
 
174
- Resolution order:
175
- 1. prompts/{name}/ (directory TXT format)
176
- 2. prompts/{name}.yaml (flat YAML layout)
177
- 3. prompts/_library/{name}.yaml (legacy layout)
178
  """
179
- # Directory TXT format (new)
180
- dir_path = prompts_dir / prompt_name
181
- if dir_path.is_dir():
182
- return dir_path
183
- # Flat YAML layout
184
- yaml_path = prompts_dir / f"{prompt_name}.yaml"
185
- if yaml_path.exists():
186
- return yaml_path
187
- # Legacy _library layout
188
- legacy_path = prompts_dir / "_library" / f"{prompt_name}.yaml"
189
- if legacy_path.exists():
190
- return legacy_path
191
- raise click.ClickException(
192
- f"Prompt '{prompt_name}' not found. Searched:\n"
193
- f" {dir_path}/ (directory)\n"
194
- f" {yaml_path} (YAML)\n"
195
- f" {legacy_path} (legacy)"
196
- )
197
-
198
-
199
- def _resolve_pipeline_file(project_dir: Path, pipeline_name: str) -> Path:
200
- """Resolve pipeline YAML from pipelines/ directory."""
201
- path = project_dir / "pipelines" / f"{pipeline_name}.yaml"
202
- if not path.exists():
203
- raise click.ClickException(f"Pipeline '{pipeline_name}' not found: {path}")
204
- return path
205
-
206
-
207
- def _compose_pipeline_config(project_dir: Path, pipeline_name: str, overrides: dict) -> dict:
208
- """ํŒŒ์ดํ”„๋ผ์ธ์„ `extends`/override ๊นŒ์ง€ ํ•ด์„ํ•ด ๋Œ๋ ค์ค€๋‹ค.
209
-
210
- ์กฐ๋ฆฝ ์‹คํŒจ(์—†๋Š” base, ์—†๋Š” ์Šคํ…์„ ๊ฐ€๋ฆฌํ‚ค๋Š” override ๋“ฑ)๋Š” run ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—
211
- ์ฃฝ์ธ๋‹ค โ€” ํšจ๊ณผ ์—†๋Š” override ๋กœ ๋Œ์•„๊ฐ„ run ์€ ๊ฒฐ๋ก ์„ ์กฐ์šฉํžˆ ์˜ค์—ผ์‹œํ‚จ๋‹ค.
212
- """
213
- from solar_eval.core.pipeline_compose import PipelineCompositionError, compose_pipeline
214
- from solar_eval.core.project_loader import load_pipeline_file
215
-
216
- pipelines_dir = _resolve_pipeline_file(project_dir, pipeline_name).parent
217
- try:
218
- composed = load_pipeline_file(pipelines_dir, pipeline_name)
219
- if overrides:
220
- composed = compose_pipeline(composed, overrides=overrides)
221
- return composed
222
- except (PipelineCompositionError, FileNotFoundError) as e:
223
- raise click.ClickException(str(e)) from None
224
 
225
 
226
  def _build_run_name(task: str, pipeline_name: str, prompt_name: str) -> str:
@@ -241,7 +204,7 @@ def _make_run_dir(
241
  ๊ฐ™์€ ์ดˆ์— ๊ฐ™์€ (task, pipeline, prompt) ์กฐํ•ฉ์œผ๋กœ ๋‘ run ์ด ์‹œ์ž‘๋˜๋ฉด
242
  `_build_run_name` ์ด ๊ฐ™์€ ์ด๋ฆ„์„ ๋‚ธ๋‹ค. `mkdir(exist_ok=False)` ๋ฅผ ์“ฐ๋ฉด ๋‘ ํ”„๋กœ์„ธ์Šค๊ฐ€
243
  ๋™์‹œ์— ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ mkdir ํ•ด๋„ ์ •ํ™•ํžˆ ํ•˜๋‚˜๋งŒ ์„ฑ๊ณตํ•˜๊ณ , ์ง„ ์ชฝ์€ ์ƒˆ ํƒ€์ž„์Šคํƒฌํ”„๋กœ
244
- ์žฌ์‹œ๋„ํ•œ๋‹ค โ€” ํŒŒ์ผ ํฌ๋งท ์ž์ฒด๋Š” ์•ˆ ๋ฐ”๊พธ๋ฏ€๋กœ evalhub ์˜ run_id ํŒŒ์‹ฑ๊ณผ ํ˜ธํ™˜๋œ๋‹ค.
245
  """
246
  for _ in range(_RUN_DIR_MAX_ATTEMPTS):
247
  run_name = _build_run_name(task, pipeline_name, prompt_name)
@@ -254,254 +217,104 @@ def _make_run_dir(
254
  raise click.ClickException(f"Could not allocate a unique run directory under {artifacts_dir}")
255
 
256
 
257
- def _config_source(cfg, project: str) -> str:
258
- """run ์˜ config ์ถœ์ฒ˜ โ€” ๋ ˆํฌ ๊ด€๋ฆฌ ํ”„๋กœ์ ํŠธ๋ฉด ๋ ˆํฌ ์ƒ๋Œ€ ๊ฒฝ๋กœ, ์•„๋‹ˆ๋ฉด ๋ฐ์ดํ„ฐ ํ™ˆ ์ ˆ๋Œ€ ๊ฒฝ๋กœ."""
259
- config_dir = cfg.config_dir(project)
260
- if cfg.is_repo_managed(project) and cfg.repo_root is not None:
261
- return str(config_dir.relative_to(cfg.repo_root))
262
- return str(config_dir)
263
-
264
-
265
- def _git_commit(repo_root: Path | None) -> str | None:
266
- """์žฌํ˜„์šฉ ๋„์žฅ: ๋ ˆํฌ HEAD ์ปค๋ฐ‹. ์ž‘์—… ํŠธ๋ฆฌ๊ฐ€ ๋”๋Ÿฌ์šฐ๋ฉด '-dirty' ๋ฅผ ๋ถ™์ธ๋‹ค."""
267
- if repo_root is None:
268
- return None
269
- try:
270
- head = subprocess.run(
271
- ["git", "-C", str(repo_root), "rev-parse", "--short", "HEAD"],
272
- capture_output=True,
273
- text=True,
274
- check=True,
275
- ).stdout.strip()
276
- status = subprocess.run(
277
- ["git", "-C", str(repo_root), "status", "--porcelain"],
278
- capture_output=True,
279
- text=True,
280
- check=True,
281
- ).stdout.strip()
282
- return f"{head}-dirty" if status else head
283
- except (OSError, subprocess.CalledProcessError):
284
- return None
285
-
286
-
287
  def _start_local(
288
  cfg,
289
- project: str,
290
- task: str,
291
- prompt_name: str,
292
- model: str,
293
- pipeline_name: str | None,
294
- reasoning_effort: str | None,
295
- temperature: float,
296
- max_tokens: int,
297
- max_workers: int,
298
- limit: int | None,
299
  note: str | None = None,
300
- set_options: tuple[str, ...] = (),
301
  no_ingest: bool = False,
302
- ) -> None:
303
- """Start run locally with the new prompt/model/pipeline interface."""
304
- from solar_eval.core.dataset_loader import DatasetLoader
305
- from solar_eval.core.fingerprint import pipeline_fingerprint, prompt_fingerprint
306
- from solar_eval.core.pipeline_compose import PipelineCompositionError, parse_set_option
307
- from solar_eval.core.project_loader import load_all_project_configs
308
- from solar_eval.core.runner import BatchRunner
309
- from solar_eval.models.prompt_version import (
310
- detect_prompt_format,
311
- load_prompt_messages,
312
- load_step_prompts,
313
- )
314
- from solar_eval.providers import UpstageProvider
315
- from solar_eval.providers.openai_provider import OpenAIProvider
 
 
 
 
 
 
316
  from solar_eval.stores import JsonlStore
317
 
318
- # Load project config
319
- configs = load_all_project_configs(cfg.projects_dir, cfg.config_dirs)
320
- project_config = next((c for c in configs if c["name"] == project), None)
321
- if not project_config:
322
- raise click.ClickException(f"Project not found in {cfg.projects_dir}: {project}")
323
-
324
- task_config = next((t for t in project_config.get("tasks", []) if t["name"] == task), None)
325
- if not task_config:
326
- raise click.ClickException(f"Task '{task}' not found in project '{project}'")
327
-
328
- # Resolve prompt file
329
- prompts_dir = cfg.prompts_dir(project)
330
- prompt_path = _resolve_prompt(prompts_dir, prompt_name)
331
-
332
- fmt = detect_prompt_format(prompt_path)
333
- if fmt == "multi_step":
334
- step_prompts = load_step_prompts(prompt_path)
335
- prompt = {
336
- "step_prompts": step_prompts,
337
- "model": model,
338
- "temperature": temperature,
339
- "max_tokens": max_tokens,
340
- }
341
- else:
342
- messages = load_prompt_messages(prompt_path)
343
- prompt = {
344
- "messages": messages,
345
- "model": model,
346
- "temperature": temperature,
347
- "max_tokens": max_tokens,
348
- }
349
-
350
- if reasoning_effort:
351
- prompt["reasoning_effort"] = reasoning_effort
352
-
353
- # Resolve pipeline โ€” config ๋Š” ๋ ˆํฌ ๊ด€๋ฆฌ ํ”„๋กœ์ ํŠธ๋ฉด git ์ชฝ์ด ์ •๋ณธ.
354
- # `extends` ์ƒ์†๊ณผ `--set` override ๋Š” ์—ฌ๊ธฐ์„œ ์ „๋ถ€ ํ•ด์„๋ผ, ์‹คํ–‰ ์—”์ง„์—๋Š” ์™„์„ฑ๋œ
355
- # steps ๋ชฉ๋ก๋งŒ ๋„˜์–ด๊ฐ„๋‹ค.
356
- try:
357
- cli_overrides = parse_set_option(set_options)
358
- except PipelineCompositionError as e:
359
- raise click.ClickException(str(e)) from None
360
-
361
- effective_pipeline = pipeline_name
362
- if pipeline_name:
363
- pipeline_config = _compose_pipeline_config(
364
- cfg.config_dir(project), pipeline_name, cli_overrides
365
- )
366
- task_config = {**task_config, "pipeline_config": pipeline_config}
367
- else:
368
- # Use default from project.yaml (already resolved by project_loader)
369
- pc = task_config.get("pipeline_config", {})
370
- effective_pipeline = pc.get("name", "default") if isinstance(pc, dict) else str(pc)
371
- if cli_overrides:
372
- raise click.ClickException("--set requires an explicit --pipeline/-P")
373
- pipeline_config = pc if isinstance(pc, dict) else {}
374
-
375
- # Build run name and directory -- ์ด๋ฆ„ ์ถฉ๋Œ์€ ์—ฌ๊ธฐ์„œ ์›์ž์ ์œผ๋กœ ๊ฑธ๋Ÿฌ์ง„๋‹ค(F10).
376
  run_name, run_dir = _make_run_dir(
377
- cfg.artifacts_dir(project), task, effective_pipeline, prompt_name
378
- )
379
- store = JsonlStore(run_dir)
380
-
381
- # Save run metadata
382
- run_meta = {
383
- "task": task,
384
- "model": model,
385
- "prompt": prompt_name,
386
- "pipeline": effective_pipeline,
387
- "temperature": temperature,
388
- "max_tokens": max_tokens,
389
- "reasoning_effort": reasoning_effort,
390
- # ์™œ ๋Œ๋ ธ๋Š”์ง€ โ€” ์˜ˆ์ „์—๋Š” ๋ณ„๋„ ๋žฉ๋…ธํŠธ(experiments/*.yaml)๊ฐ€ ๋‹ด์•˜์ง€๋งŒ run ๊ณผ ๋ถ„๋ฆฌ๋ผ ์žˆ์–ด
391
- # ์ฐธ์กฐ๊ฐ€ ๋Š์–ด์ง€๊ณ  ์ฉ์—ˆ๋‹ค. run ๊ณผ ๊ฐ™์€ ํŒŒ์ผ์— ๋‘๋ฉด ๋Š์–ด์งˆ ์ˆ˜๊ฐ€ ์—†๋‹ค.
392
- "note": note,
393
- # provenance: ์ด run ์˜ config(ํ”„๋กฌํ”„ํŠธยทํŒŒ์ดํ”„๋ผ์ธ)๊ฐ€ ์–ด๋””์„œ ์™”๋Š”์ง€ (W&B/DVC ์‹ ๋„์žฅ)
394
- "config_source": _config_source(cfg, project),
395
- "git_commit": _git_commit(cfg.repo_root) if cfg.is_repo_managed(project) else None,
396
- # ๋‚ด์šฉ ์ง€๋ฌธ โ€” ์ด๋ฆ„์ด ์•„๋‹ˆ๋ผ ๋‚ด์šฉ์œผ๋กœ ์„ค์ •์„ ์‹๋ณ„ํ•œ๋‹ค. ์ด๋ฆ„๋งŒ ๋‹ค๋ฅธ ์‚ฌ๋ณธ,
397
- # `extends` ๋กœ ์กฐ๋ฆฝํ•œ ๊ฒƒ, `--set` ์œผ๋กœ ์ฆ‰์„์—์„œ ๋งŒ๋“  ๊ฒƒ์ด ๋ชจ๋‘ ๊ฐ™์€ ๊ฐ’์ด๋ฉด
398
- # ๊ฐ™์€ ์ง€๋ฌธ์„ ๊ฐ–๋Š”๋‹ค (core/fingerprint.py).
399
- "prompt_sha": prompt_fingerprint(prompt_path),
400
- "pipeline_sha": pipeline_fingerprint(pipeline_config),
401
- # ํŒŒ์ผ ์—†์ด ๋Œ๋ฆฐ ์‹คํ—˜์ด๋ฉด ๋ฌด์—‡์„ ๋ฐ”๊ฟจ๋Š”์ง€๊ฐ€ ์—ฌ๊ธฐ ๋‚จ๋Š”๋‹ค.
402
- "pipeline_overrides": cli_overrides or None,
403
- }
404
- # run_dir ์€ _make_run_dir ์ด ์ด๋ฏธ ์›์ž์ ์œผ๋กœ ๋งŒ๋“ค์—ˆ๋‹ค -- ์—ฌ๊ธฐ์„œ ๋‹ค์‹œ mkdir ํ•˜์ง€ ์•Š๋Š”๋‹ค.
405
- (run_dir / "run.json").write_text(json.dumps(run_meta, ensure_ascii=False, indent=2))
406
-
407
- # Log run info
408
- click.secho(f"Prompt: {prompt_name} [{fmt}]", fg="blue")
409
- click.secho(f"Model: {model}", fg="blue")
410
- click.secho(f"Pipeline: {effective_pipeline}", fg="blue")
411
- for path, value in cli_overrides.items():
412
- click.secho(f" override: {path} = {value!r}", fg="magenta")
413
- click.secho(
414
- f"Fingerprint: prompt {run_meta['prompt_sha']} / pipeline {run_meta['pipeline_sha']}",
415
- fg="blue",
416
  )
417
- if reasoning_effort:
418
- click.secho(f"Reasoning effort: {reasoning_effort}", fg="blue")
419
- if limit:
420
- click.secho(f"Limit: {limit} samples", fg="yellow")
421
- click.secho(f"Starting local run: {project}/{run_name}", fg="blue")
422
- click.secho(f"Output: {run_dir}", fg="blue")
423
-
424
- # Create runner
425
- provider = UpstageProvider()
426
- judge_provider = OpenAIProvider()
427
- dataset_loader = DatasetLoader(base_dir=cfg.projects_dir)
428
- runner = BatchRunner(
429
- store=store,
430
- inference_provider=provider,
431
- judge_provider=judge_provider,
432
- dataset_loader=dataset_loader,
433
- )
434
-
435
- # Run with progress display
436
- _run_with_progress(runner, run_name, project_config, task_config, prompt, max_workers, limit)
437
 
438
  _warn_if_incomplete(run_dir)
 
 
439
  click.echo(f"\nResults saved to: {run_dir}/")
440
- if not no_ingest:
441
- _ingest_into_evalhub(cfg, project, run_dir)
 
 
 
 
442
 
443
 
444
- def _ingest_into_evalhub(cfg, project: str, run_dir: Path) -> None:
445
- """๋ฐฉ๊ธˆ ๋งŒ๋“  run ์„ evalhub ๋กœ ์ ์žฌํ•œ๋‹ค.
 
 
446
 
447
- **์‹คํŒจํ•ด๋„ run ์€ ์„ฑ๊ณต์ด๋‹ค** โ€” ๊ฒฐ๊ณผ๋Š” ์ด๋ฏธ ๋””์Šคํฌ์— ์žˆ๊ณ , ์ ์žฌ๋Š” ๋‚˜์ค‘์— ๋‹ค์‹œ
448
- ๋Œ๋ฆฌ๋ฉด ๋˜๋Š” ๋ฉฑ๋“ฑ ์ž‘์—…์ด๋‹ค. ๊ทธ๋ž˜์„œ ์–ด๋–ค ์‹คํŒจ๋„ exit code ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๊ณ 
449
- "๋ฌด์—‡์ด ์•ˆ ๋๊ณ  ์–ด๋–ป๊ฒŒ ๋˜์‚ด๋ฆฌ๋Š”์ง€"๋งŒ ์•Œ๋ฆฐ๋‹ค. ์ ์žฌ๋ฅผ ๋ชป ํ–ˆ๋‹ค๋Š” ์‚ฌ์‹ค ์ž์ฒด๋ฅผ
450
- ์กฐ์šฉํžˆ ๋„˜๊ธฐ์ง€๋„ ์•Š๋Š”๋‹ค โ€” ๊ทธ๋Ÿฌ๋ฉด evalhub ํ™”๋ฉด์ด ๋‚ก์€ ์ฑ„๋กœ ๋‚จ๋Š”๋‹ค.
451
 
452
- ๋ ˆํฌ ๋ฐ– standalone ์„ค์น˜์—๋Š” evalhub ์ž์ฒด๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์กฐ์šฉํžˆ ๊ฑด๋„ˆ๋›ด๋‹ค.
453
- """
454
- if cfg.repo_root is None:
455
- return
456
 
457
- click.secho("\nevalhub ์ ์žฌ ์ค‘...", fg="blue")
458
- try:
459
- result = subprocess.run(
460
- ["pnpm", "--filter", "@poc/eval-store", "ingest", "--only", project],
461
- cwd=str(cfg.repo_root),
462
- capture_output=True,
463
- text=True,
464
- timeout=INGEST_TIMEOUT_SECONDS,
465
- )
466
- except FileNotFoundError:
467
- _warn_ingest_skipped(project, run_dir, "pnpm ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
468
- return
469
- except subprocess.TimeoutExpired:
470
- _warn_ingest_skipped(project, run_dir, f"{INGEST_TIMEOUT_SECONDS}์ดˆ ์•ˆ์— ๋๋‚˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
471
- return
472
- except OSError as e:
473
- _warn_ingest_skipped(project, run_dir, str(e))
474
- return
475
-
476
- if result.returncode == 0:
477
- click.secho(f" evalhub ์ ์žฌ ์™„๋ฃŒ โ€” {project}", fg="green")
478
- return
479
- _warn_ingest_skipped(project, run_dir, _ingest_failure_reason(result))
480
-
481
-
482
- def _ingest_failure_reason(result: subprocess.CompletedProcess[str]) -> str:
483
- """ingest ์‹คํŒจ ์ถœ๋ ฅ์—์„œ ์‚ฌ๋žŒ์ด ์ฝ์„ ํ•œ ์ค„์„ ๋ฝ‘๋Š”๋‹ค.
484
 
485
- stderr ์ „์ฒด๋Š” node ์ŠคํƒํŠธ๋ ˆ์ด์Šค๋ผ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ์ฃผ๋ฉด ์ •์ž‘ ์›์ธ์ด ๋ฌปํžŒ๋‹ค.
486
- ๊ฐ€์žฅ ํ”ํ•œ ์›์ธ(๋ฐฑ์—”๋“œ ๋ฏธ๊ธฐ๋™)์€ ์—ฐ๊ฒฐ ๊ฑฐ๋ถ€๋กœ ๋‚˜ํƒ€๋‚˜๋ฏ€๋กœ ๋จผ์ € ์งš๋Š”๋‹ค.
487
- """
488
- output = f"{result.stderr}\n{result.stdout}"
489
- if "ECONNREFUSED" in output or "connect ECONNREFUSED" in output:
490
- return "Postgres ์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (๋ฐฑ์—”๋“œ๊ฐ€ ๊บผ์ ธ ์žˆ๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค)"
491
- for line in result.stderr.splitlines():
492
- stripped = line.strip()
493
- if stripped and not stripped.startswith("at ") and "node_modules" not in stripped:
494
- return stripped[:160]
495
- return f"ingest ๊ฐ€ exit {result.returncode} ๋กœ ๋๋‚ฌ์Šต๋‹ˆ๋‹ค"
 
 
 
 
 
 
 
 
 
 
 
 
496
 
497
 
498
- def _warn_ingest_skipped(project: str, run_dir: Path, reason: str) -> None:
499
- """์ ์žฌ ์‹คํŒจ๋ฅผ ์•Œ๋ฆฌ๊ณ  ๋˜์‚ด๋ฆฌ๋Š” ๋ช…๋ น์„ ๊ทธ๋Œ€๋กœ ์‹ค์–ด ์ค€๋‹ค (exit code ๋Š” 0 ์œ ์ง€)."""
500
- click.secho(f"\n โš  evalhub ์ ์žฌ๋ฅผ ๊ฑด๋„ˆ๋›ฐ์—ˆ์Šต๋‹ˆ๋‹ค โ€” {reason}", fg="yellow", bold=True)
501
- click.secho(f" run ๊ฒฐ๊ณผ๋Š” ๊ทธ๋Œ€๋กœ ์žˆ์Šต๋‹ˆ๋‹ค: {run_dir}", fg="yellow")
502
- click.secho(" ๋ฐฑ์—”๋“œ๋ฅผ ๋„์šด ๋’ค ์•„๋ž˜๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค:", fg="yellow")
503
- click.secho(" make evalhub-up", fg="yellow")
504
- click.secho(f" make evalhub-ingest ONLY={project}", fg="yellow")
 
 
 
505
 
506
 
507
  def _warn_if_incomplete(run_dir: Path) -> None:
@@ -535,141 +348,6 @@ def _warn_if_incomplete(run_dir: Path) -> None:
535
  )
536
 
537
 
538
- def _run_with_progress(
539
- runner,
540
- run_id,
541
- project_config,
542
- task_config,
543
- prompt,
544
- max_workers,
545
- limit,
546
- completed_results: list[dict[str, Any]] | None = None,
547
- ):
548
- """Execute a run with Rich progress bar display.
549
-
550
- `completed_results` ๋Š” resume ์ด ์ด๋ฏธ ๋๋‚œ ์ƒ˜ํ”Œ์„ ๋‹ค์‹œ ๋Œ๋ฆฌ์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ๋‹ค๋ฆฌ๋‹ค --
551
- ์—ฌ๊ธฐ์„œ ์•ˆ ๋„˜๊ธฐ๋ฉด runner ๊ฐ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๊ฐ–๊ณ  ์žˆ์–ด๋„ ์†Œ์šฉ์ด ์—†๋‹ค(F2).
552
-
553
- `execute_run` ์€ ์น˜๋ช…์  ์‹คํŒจ์—์„œ raise ํ•˜๋ฏ€๋กœ ์ด ํ•จ์ˆ˜๋„ raise ํ•œ๋‹ค --
554
- ์ผ๋ถ€๋Ÿฌ ๊ฐ์‹ธ์ง€ ์•Š๋Š”๋‹ค. ์œ„์ชฝ `on_progress` ์˜ "error" ์ด๋ฒคํŠธ๊ฐ€ ์ด๋ฏธ ์‚ฌ๋žŒ์ด ์ฝ์„
555
- ๋ฉ”์‹œ์ง€๋ฅผ ์ฐ์—ˆ๊ณ , ์—ฌ๊ธฐ์„œ ๋˜ ๊ฐ์‹ธ๋ฉด ๊ฐ™์€ ๋‚ด์šฉ์ด ๋‘ ๋ฒˆ ๋‚˜์˜จ๋‹ค.
556
- """
557
- import logging as _logging
558
-
559
- from rich.console import Console
560
- from rich.live import Live
561
- from rich.text import Text
562
-
563
- console = Console(stderr=True)
564
- use_rich = True
565
- live_ctx: Live | None = None
566
-
567
- progress_bar = Progress(
568
- SpinnerColumn(),
569
- TextColumn("[progress.description]{task.description}"),
570
- BarColumn(),
571
- TextColumn("{task.completed}/{task.total}"),
572
- TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
573
- TimeElapsedColumn(),
574
- )
575
-
576
- try:
577
- live_ctx = Live(progress_bar, console=console, refresh_per_second=8)
578
- live_ctx.start()
579
- except Exception:
580
- use_rich = False
581
-
582
- # Redirect pipeline/runner logs above the progress bar
583
- class _LiveLogHandler(_logging.Handler):
584
- def emit(self, record):
585
- if live_ctx:
586
- live_ctx.console.print(Text(record.getMessage(), style="dim"))
587
-
588
- _log_names = (
589
- "solar_eval.pipelines.pipeline",
590
- "solar_eval.pipelines.steps.llm",
591
- "solar_eval.core.runner",
592
- "solar_eval.providers.upstage",
593
- )
594
- if use_rich:
595
- _live_handler = _LiveLogHandler()
596
- for name in _log_names:
597
- lg = _logging.getLogger(name)
598
- lg.addHandler(_live_handler)
599
- lg.propagate = False
600
- _logging.getLogger("httpx").setLevel(_logging.WARNING)
601
-
602
- def _cleanup_live():
603
- if use_rich:
604
- live_ctx.stop()
605
- for name in _log_names:
606
- _logging.getLogger(name).removeHandler(_live_handler)
607
- _logging.getLogger(name).propagate = True
608
-
609
- inference_task_id = None
610
- eval_task_id = None
611
-
612
- async def on_progress(event):
613
- nonlocal inference_task_id, eval_task_id
614
- etype = event.get("type")
615
- if etype == "dataset_loaded":
616
- total = event["total"]
617
- if use_rich:
618
- inference_task_id = progress_bar.add_task("Inference", total=total)
619
- else:
620
- click.secho(f" Dataset loaded: {total} samples", fg="blue")
621
- elif etype == "inference_progress":
622
- if use_rich and inference_task_id is not None:
623
- progress_bar.update(inference_task_id, completed=event["completed"])
624
- elif not use_rich:
625
- c, t = event["completed"], event["total"]
626
- if c % 10 == 0 or c == t:
627
- click.echo(f" Inference: {c}/{t}")
628
- elif etype == "inference_complete":
629
- if use_rich and inference_task_id is not None:
630
- progress_bar.update(inference_task_id, completed=event["total"])
631
- else:
632
- click.secho(f" Inference complete: {event['total']} samples", fg="green")
633
- elif etype == "evaluation_start":
634
- total = event.get("total", 0)
635
- if use_rich:
636
- eval_task_id = progress_bar.add_task("Evaluation", total=total or 1)
637
- else:
638
- click.secho(" Starting evaluation...", fg="blue")
639
- elif etype == "evaluation_progress":
640
- if use_rich and eval_task_id is not None:
641
- progress_bar.update(eval_task_id, completed=event["completed"])
642
- elif etype == "done":
643
- if use_rich and eval_task_id is not None:
644
- progress_bar.update(eval_task_id, completed=progress_bar.tasks[eval_task_id].total)
645
- _cleanup_live()
646
- click.echo()
647
- click.secho("Run completed!", fg="green", bold=True)
648
- overall = event.get("overall_score")
649
- if overall is not None:
650
- click.echo(f" Overall Score: {overall:.4f}")
651
- scores = event.get("scores", {})
652
- if scores:
653
- click.echo(f" Scores: {format_scores(scores)}")
654
- elif etype == "error":
655
- _cleanup_live()
656
- click.echo()
657
- click.secho(f" Run failed: {event.get('error')}", fg="red", bold=True)
658
-
659
- asyncio.run(
660
- runner.execute_run(
661
- run_id=run_id,
662
- project_config=project_config,
663
- task_config=task_config,
664
- prompt=prompt,
665
- on_progress=on_progress,
666
- max_workers=max_workers,
667
- limit=limit,
668
- completed_results=completed_results,
669
- )
670
- )
671
-
672
-
673
  @runs_group.command("batch")
674
  @click.argument("project")
675
  @click.option("--task", "-t", required=True, help="Task name")
@@ -684,331 +362,101 @@ def _run_with_progress(
684
  )
685
  @click.option("--reasoning-effort", "-re", default=None, help="Reasoning effort")
686
  @click.option("--max-workers", "-w", default=5, type=int, help="Max parallel workers")
 
 
 
687
  @click.pass_context
688
- def batch_runs(ctx, project, task, prompts, model, pipeline_name, reasoning_effort, max_workers):
689
- """Run evaluation for multiple prompts sequentially."""
 
 
 
 
690
  cfg = ctx.obj["config"]
691
  if cfg.is_remote:
692
  raise click.ClickException("Batch is local-mode only")
693
 
694
  prompt_list = [p.strip() for p in prompts.split(",")]
 
 
 
 
 
695
  click.secho(f"Batch run: {project}/{task} โ€” prompts: {prompt_list}", fg="blue", bold=True)
696
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
697
  for pname in prompt_list:
698
  click.secho(f"\n{'=' * 50}", fg="blue")
699
  click.secho(f"Running prompt: {pname}...", fg="blue", bold=True)
700
  click.secho(f"{'=' * 50}", fg="blue")
701
  try:
702
- _start_local(
703
- cfg,
704
- project=project,
705
- task=task,
706
- prompt_name=pname,
707
- model=model,
708
- pipeline_name=pipeline_name,
709
- reasoning_effort=reasoning_effort,
710
- temperature=0.0,
711
- max_tokens=4000,
712
- max_workers=max_workers,
713
- limit=None,
714
  )
715
  except Exception as e:
 
716
  click.secho(f" {pname} failed: {e}", fg="red")
717
-
718
- click.secho(f"\n{'=' * 50}", fg="blue")
719
- click.secho("Batch complete!", fg="green", bold=True)
720
 
721
 
722
- @runs_group.command("resume")
723
- @click.argument("project")
724
- @click.option("--run-id", "-r", required=True, help="Run ID to resume")
725
- @click.option("--max-workers", "-w", default=5, type=int, help="Max parallel workers")
726
- @click.pass_context
727
- def resume_run(ctx: click.Context, project: str, run_id: str, max_workers: int) -> None:
728
- """Resume an interrupted evaluation run."""
729
- cfg = ctx.obj["config"]
730
- if cfg.is_remote:
731
- raise click.ClickException("Resume is local-mode only")
732
-
733
- from solar_eval.core.dataset_loader import DatasetLoader
734
- from solar_eval.core.project_loader import load_all_project_configs
735
- from solar_eval.core.runner import BatchRunner
736
- from solar_eval.providers import UpstageProvider
737
- from solar_eval.providers.openai_provider import OpenAIProvider
738
- from solar_eval.stores import JsonlStore
739
-
740
- # Find the run directory
741
- run_dir = cfg.artifacts_dir(project) / run_id
742
- if not run_dir.exists():
743
- raise click.ClickException(f"Run directory not found: {run_dir}")
744
-
745
- store = JsonlStore(run_dir)
746
- run_meta = store.load_run_meta()
747
-
748
- if not run_meta:
749
- raise click.ClickException(f"No run.json found in {run_dir}")
750
-
751
- status = run_meta.get("status", "unknown")
752
- if status == "completed":
753
- raise click.ClickException(f"Run {run_id} is already completed. Nothing to resume.")
754
-
755
- task = run_meta.get("task")
756
- if not task:
757
- raise click.ClickException("Cannot determine task from run.json")
758
-
759
- # Load existing results
760
- existing_results = store.load_existing_results()
761
- completed_indices = {r["sample_idx"] for r in existing_results}
762
- click.secho(f"Resuming run: {run_id}", fg="blue", bold=True)
763
- click.secho(f" Task: {task}, Already completed: {len(completed_indices)} samples", fg="blue")
764
-
765
- # Load project/task config
766
- configs = load_all_project_configs(cfg.projects_dir, cfg.config_dirs)
767
- project_config = next((c for c in configs if c["name"] == project), None)
768
- if not project_config:
769
- raise click.ClickException(f"Project not found: {project}")
770
-
771
- task_config = next((t for t in project_config.get("tasks", []) if t["name"] == task), None)
772
- if not task_config:
773
- raise click.ClickException(f"Task '{task}' not found in project '{project}'")
774
-
775
- # Resolve prompt from run.json
776
- prompt_name = run_meta.get("prompt")
777
- pipeline_name = run_meta.get("pipeline")
778
- model = run_meta.get("model", "solar-pro2")
779
-
780
- if not prompt_name:
781
- raise click.ClickException(
782
- "Cannot determine prompt from run.json. Only runs created with the new CLI can be resumed."
783
- )
784
 
785
- from solar_eval.models.prompt_version import (
786
- detect_prompt_format,
787
- load_prompt_messages,
788
- load_step_prompts,
789
- )
790
-
791
- prompt_path = _resolve_prompt(cfg.prompts_dir(project), prompt_name)
792
- fmt = detect_prompt_format(prompt_path)
793
- if fmt == "multi_step":
794
- prompt = {
795
- "step_prompts": load_step_prompts(prompt_path),
796
- "model": model,
797
- "temperature": run_meta.get("temperature", 0.0),
798
- "max_tokens": run_meta.get("max_tokens", 4000),
799
- }
800
- else:
801
- prompt = {
802
- "messages": load_prompt_messages(prompt_path),
803
- "model": model,
804
- "temperature": run_meta.get("temperature", 0.0),
805
- "max_tokens": run_meta.get("max_tokens", 4000),
806
- }
807
- if run_meta.get("reasoning_effort"):
808
- prompt["reasoning_effort"] = run_meta["reasoning_effort"]
809
-
810
- # Override pipeline if specified โ€” ์›๋ž˜ run ์ด `--set` ์œผ๋กœ ๋ฐ”๊ฟ” ๋Œ๋ฆฐ ๊ฒƒ์ด๋ฉด
811
- # ๊ทธ override ๊นŒ์ง€ ๋ณต์›ํ•ด์•ผ ๊ฐ™์€ ์„ค์ •์œผ๋กœ ์ด์–ด์ง„๋‹ค.
812
- if pipeline_name and pipeline_name != "default":
813
- task_config = {
814
- **task_config,
815
- "pipeline_config": _compose_pipeline_config(
816
- cfg.config_dir(project),
817
- pipeline_name,
818
- run_meta.get("pipeline_overrides") or {},
819
- ),
820
- }
821
-
822
- click.secho(f" Model: {model}", fg="blue")
823
-
824
- # ์ด์ „ ํ‰๊ฐ€๋ฅผ ๋ฏธ๋ฆฌ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค(F3) -- ์žฌํ‰๊ฐ€๊ฐ€ ๋„์ค‘์— ์ฃฝ์œผ๋ฉด ๋ณต๊ตฌํ•  ์ ์ˆ˜๊ฐ€
825
- # ์—†์–ด์ง„๋‹ค. create_evaluation() ์ด ์›์ž์  ๊ต์ฒด๋ผ ์ง€์šฐ์ง€ ์•Š์•„๋„ ์•ˆ์ „ํ•˜๋‹ค.
826
-
827
- # Setup providers and runner
828
- provider = UpstageProvider()
829
- judge_provider = OpenAIProvider()
830
- dataset_loader = DatasetLoader(base_dir=cfg.projects_dir)
831
- runner = BatchRunner(
832
- store=store,
833
- inference_provider=provider,
834
- judge_provider=judge_provider,
835
- dataset_loader=dataset_loader,
836
- )
837
-
838
- _run_with_progress(
839
- runner,
840
- run_id,
841
- project_config,
842
- task_config,
843
- prompt,
844
- max_workers,
845
- limit=None,
846
- # ์ด๋ฏธ ๋๋‚œ ์ƒ˜ํ”Œ์€ ๊ฑด๋„ˆ๋›ด๋‹ค -- ์•ˆ ๋„˜๊ธฐ๋ฉด resume ์ด ์ „ ์ƒ˜ํ”Œ์„ ๋‹ค์‹œ ์œ ๋ฃŒ ํ˜ธ์ถœํ•˜๊ณ 
847
- # results.jsonl ์— ๊ฐ™์€ sample_idx ๋ฅผ ์ค‘๋ณต append ํ•œ๋‹ค(F2).
848
- completed_results=existing_results,
849
- )
850
-
851
- click.echo(f"\nResults saved to: {run_dir}/")
852
-
853
-
854
- @runs_group.command("eval")
855
- @click.argument("project")
856
- @click.option(
857
- "--run-id",
858
- "-r",
859
- required=True,
860
- help="Run ID to evaluate",
861
- )
862
- @click.pass_context
863
- def eval_run(ctx: click.Context, project: str, run_id: str) -> None:
864
- """Run evaluation only on existing inference results (no new inference)."""
865
- cfg = ctx.obj["config"]
866
- if cfg.is_remote:
867
- raise click.ClickException("Eval is local-mode only")
868
-
869
- from solar_eval.core.project_loader import load_all_project_configs
870
- from solar_eval.core.runner import build_eval_sample_from_result
871
- from solar_eval.evaluators.registry import create_evaluator
872
- from solar_eval.stores import JsonlStore
873
-
874
- # Find run directory
875
- run_dir = cfg.artifacts_dir(project) / run_id
876
- if not run_dir.exists():
877
- raise click.ClickException(f"Run directory not found: {run_dir}")
878
-
879
- store = JsonlStore(run_dir)
880
- run_meta = store.load_run_meta()
881
- if not run_meta:
882
- raise click.ClickException(f"No run.json found in {run_dir}")
883
-
884
- task = run_meta.get("task")
885
- if not task:
886
- raise click.ClickException("Cannot determine task from run.json")
887
-
888
- # Load results
889
- results = store.load_existing_results()
890
- if not results:
891
- raise click.ClickException(f"No inference results found in {run_dir}/results.jsonl")
892
-
893
- click.secho(f"Evaluating run: {run_id}", fg="blue", bold=True)
894
- click.secho(f" Task: {task}, Samples: {len(results)}", fg="blue")
895
-
896
- # Load task config for evaluator settings
897
- configs = load_all_project_configs(cfg.projects_dir, cfg.config_dirs)
898
- project_config = next((c for c in configs if c["name"] == project), None)
899
- if not project_config:
900
- raise click.ClickException(f"Project not found: {project}")
901
-
902
- task_config = next((t for t in project_config.get("tasks", []) if t["name"] == task), None)
903
- if not task_config:
904
- raise click.ClickException(f"Task '{task}' not found in project '{project}'")
905
-
906
- # ์ด์ „ ํ‰๊ฐ€๋ฅผ ๋ฏธ๋ฆฌ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค(F3) -- ์•„๋ž˜ ์žฌํ‰๊ฐ€๊ฐ€ ์‹คํŒจํ•ด๋„ ์˜› ์ ์ˆ˜๋Š” ๋‚จ๋Š”๋‹ค.
907
-
908
- # Run evaluation
909
- evaluator = create_evaluator(task_config.get("evaluator", {"type": "llm_judge"}))
910
- # judge provider ๋ฅผ ์•ˆ ๋„˜๊ธฐ๋ฉด llm_judge ๊ณ„์—ด์€ ์ „ ์ƒ˜ํ”Œ์—์„œ ์‹คํŒจํ•œ๋‹ค -- ๊ทธ๋ฆฌ๊ณ 
911
- # ๊ทธ ์‹คํŒจ๊ฐ€ ์˜ˆ์ „์—” 0์  placeholder ๋กœ ๋ฎ์—ฌ ์กฐ์šฉํžˆ completed ๋กœ ๋๋‚ฌ๋‹ค(F8).
912
- from solar_eval.providers.openai_provider import OpenAIProvider
913
-
914
- judge_provider = OpenAIProvider()
915
-
916
- from datetime import timezone
917
-
918
- async def run_eval():
919
- # ์ฑ„์  ์‹คํŒจ๋Š” ์ ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ ์‹คํŒจ๋กœ ๋‚จ๊ธด๋‹ค -- aggregate() ์—๋Š” ์„ฑ๊ณต๋ถ„๋งŒ ๋„˜๊ธด๋‹ค.
920
- eval_results: list[dict[str, Any]] = []
921
- eval_failures: list[dict[str, Any]] = []
922
-
923
- for result in results:
924
- # ๋ฆฌ์ŠคํŠธ ์œ„์น˜๊ฐ€ ์•„๋‹ˆ๋ผ ์›๋ณธ sample_idx ๊ฐ€ ์ •๋ณธ์ด๋‹ค(F6).
925
- sample_idx = result["sample_idx"]
926
- try:
927
- eval_sample = build_eval_sample_from_result(result, reference=result.get("golden"))
928
- evaluator.validate_required_fields(eval_sample)
929
- eval_result = await evaluator.evaluate(sample=eval_sample, provider=judge_provider)
930
- eval_results.append({**eval_result, "sample_idx": sample_idx})
931
- except Exception as e:
932
- click.secho(f" Sample {sample_idx} eval failed: {e}", fg="yellow")
933
- eval_failures.append({"sample_idx": sample_idx, "error": str(e)})
934
-
935
- # Aggregate and save
936
- aggregated = evaluator.aggregate(eval_results)
937
- # ์„ฑ๊ณตํ•œ ์ฑ„์ ์ด ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์ ์ˆ˜ ์ž๋ฆฌ๋ฅผ ๋น„์šด๋‹ค -- 0.0 ์€ "0์ "์ด๋ผ๋Š” ์ธก์ •๊ฐ’์ด๋ผ
938
- # "์ธก์ •์ด ์—†์—ˆ๋‹ค"์™€ ๊ตฌ๋ถ„๋˜์ง€ ์•Š๋Š”๋‹ค(runner.py ์™€ ๊ฐ™์€ ๊ทœ์น™).
939
- overall_score = aggregated.get("overall_score", 0.0) if eval_results else None
940
- eval_id = await store.create_evaluation(
941
- {
942
- "run_id": run_id,
943
- "scores": aggregated.get("scores", {}),
944
- "overall_score": overall_score,
945
- "eval_model": "local",
946
- "eval_success_count": len(eval_results),
947
- "eval_failed_count": len(eval_failures),
948
- "failed_sample_indices": [f["sample_idx"] for f in eval_failures],
949
- }
950
- )
951
-
952
- # Save per-sample eval details -- ์„ฑ๊ณต/์‹คํŒจ ๋‘˜ ๋‹ค ํ•œ ํ–‰์”ฉ ๋‚จ๊ธด๋‹ค.
953
- eval_detail_docs = []
954
- for er in eval_results:
955
- eval_detail_docs.append(
956
- {
957
- "evaluation_id": eval_id,
958
- "sample_idx": er["sample_idx"],
959
- "category_scores": er.get("category_scores", {}),
960
- "error_counts": {
961
- k: v.get("error_count", 0)
962
- for k, v in er.get("details", {}).items()
963
- if isinstance(v, dict)
964
- },
965
- "severity": er.get("severity", ""),
966
- "score": er.get("score", 0.0),
967
- }
968
- )
969
- for f in eval_failures:
970
- eval_detail_docs.append(
971
- {
972
- "evaluation_id": eval_id,
973
- "sample_idx": f["sample_idx"],
974
- "category_scores": {},
975
- "error_counts": {},
976
- "severity": None,
977
- # 0.0 ์ด ์•„๋‹ˆ๋ผ None -- ์ฑ„์  ์‹คํŒจ๋ฅผ ์ตœ์ € ์ ์ˆ˜์™€ ๊ตฌ๋ถ„ํ•œ๋‹ค.
978
- "score": None,
979
- "error": f["error"],
980
- }
981
- )
982
- await store.insert_eval_details(eval_detail_docs)
983
-
984
- # Update run status
985
- await store.update_run(
986
- run_id,
987
- {
988
- "status": "completed",
989
- "completed_at": datetime.now(timezone.utc),
990
- },
991
- )
992
-
993
- return {**aggregated, "overall_score": overall_score, "failed": len(eval_failures)}
994
-
995
- aggregated = asyncio.run(run_eval())
996
-
997
- click.echo()
998
- click.secho("Evaluation completed!", fg="green", bold=True)
999
- overall = aggregated.get("overall_score")
1000
- if overall is None:
1001
- # ์ฑ„์ ์ด ํ•œ ๊ฑด๋„ ์„ฑ๊ณตํ•˜์ง€ ๋ชปํ–ˆ๋‹ค -- ์ˆซ์ž๋ฅผ ์ฐ์œผ๋ฉด "0์ "์œผ๋กœ ์ฝํžŒ๋‹ค.
1002
- click.secho(" Overall Score: ์ธก์ • ์—†์Œ (์ „ ์ƒ˜ํ”Œ ์ฑ„์  ์‹คํŒจ)", fg="red", bold=True)
1003
- else:
1004
- click.echo(f" Overall Score: {overall:.4f}")
1005
- failed = aggregated.get("failed", 0)
1006
- if failed:
1007
- click.secho(f" ์ฑ„์  ์‹คํŒจ: {failed}๊ฑด (score: null ๋กœ ๊ธฐ๋ก๋จ)", fg="yellow")
1008
- scores = aggregated.get("scores", {})
1009
- if scores:
1010
- click.echo(f" Scores: {format_scores(scores)}")
1011
- click.echo(f"\nResults saved to: {run_dir}/")
1012
 
1013
 
1014
  @runs_group.command("results")
@@ -1048,3 +496,8 @@ def show_results(ctx: click.Context, run_ref: str, limit: int | None) -> None:
1048
  results = results[:limit]
1049
  click.secho(format_results_table(results), fg="blue")
1050
  click.echo(f"\nShowing {len(results)} result(s)")
 
 
 
 
 
 
1
+ """Run management commands โ€” local execution.
2
+
3
+ `resume`ยท`eval` ๋ช…๋ น๊ณผ ์ง„ํ–‰ ํ‘œ์‹œยทrun ์ค€๋น„(run.json ๋ฉ”ํƒ€) ๋Š” 800์ค„ ์ƒํ•œ ๋•Œ๋ฌธ์— ์˜† ๋ชจ๋“ˆ๋กœ ๋–ผ์–ด ๋ƒˆ๋‹ค
4
+ (๊ณ„ํš 2026-09-24-phase1a E1): `run_resume`ยท`run_eval`ยท`run_progress`ยท`run_meta`. ์ด ๋ชจ๋“ˆ์€ `list`ยท`start`ยท
5
+ `batch`ยท`results` ์™€ run ๋””๋ ‰ํ† ๋ฆฌ ํ• ๋‹น์„ ๋งก๋Š”๋‹ค. run ์ด ๋๋‚œ ๋’ค์˜ ์ ์žฌ๋Š” `solar_eval.hub_hook` ํ›…์ด ํ•œ๋‹ค
6
+ (๊ณ„ํš 2026-09-27 L6a โ€” ๋ฒˆ๋“ค ๋‹ค๋ฆฌ โ†’ hub ๋‹ค๋ฆฌ, ๋ ˆ๊ฑฐ์‹œ ์ ์žฌ๋Š” ๋ถ€๋ฅด์ง€ ์•Š๋Š”๋‹ค: ๋งˆ์Šคํ„ฐ D-M2-4 (b)).
7
+ """
8
 
 
9
  import json
 
10
  import time
11
+ from dataclasses import replace
12
  from datetime import datetime
13
  from pathlib import Path
14
  from typing import Any
15
 
16
  import click
 
17
 
18
  from solar_eval.cli.commands.projects import _resolve_project_id
19
+ from solar_eval.cli.commands.run_eval import eval_run
20
+ from solar_eval.cli.commands.run_meta import find_task, preflight_judge_config, start_options
21
+ from solar_eval.cli.commands.run_meta import resolve_prompt as _resolve_prompt # noqa: F401 -- prompts.py ๊ฐ€ ์“ด๋‹ค
22
+ from solar_eval.cli.commands.run_progress import execute_plan
23
+ from solar_eval.cli.commands.run_resume import resume_run
24
+ from solar_eval.cli.formatters import format_results_table, format_runs_table
25
+ from solar_eval.hub_hook import HookOptions, HookResult, exit_code_for, finish_batch, run_hook
26
+ from solar_eval.hub_hook.console import echo_batch_summary
27
+ from solar_eval.hub_hook.optin import strict_requested
28
+ from solar_eval.hub_hook.target import is_smoke_run
29
+ from solar_eval.hub_hook.types import (
30
+ BATCH_PENDING,
31
+ COMMAND_BATCH,
32
+ LEG_BUNDLE,
33
+ LEG_HUB,
34
+ NO_BUNDLE,
35
+ HookMode,
36
+ LegResult,
37
+ bundle_ready,
38
+ )
39
 
40
 
41
  @click.group("runs")
 
43
  """Manage evaluation runs."""
44
 
45
 
46
+ #: `--where` ์˜ต์…˜ ๋„์›€๋ง โ€” startยทbatch ๊ฐ€ ๊ฐ™์€ ๋ฌธ๋ฒ•์„ ์“ด๋‹ค(๊ณ„ํš D1).
47
+ WHERE_HELP = (
48
+ "ํ–‰ ํ•„ํ„ฐ key=v1,v2 (๋ฐ˜๋ณต ๊ฐ€๋Šฅ, AND). tags ๋Š” ๋ชจ๋‘ ๊ฐ€์ง„ ํ–‰, category ๋Š” ๋ชฉ๋ก ์ค‘ ํ•˜๋‚˜. "
49
+ "์˜ˆ: --where tags=yul-ryul --where category=a,b"
50
+ )
51
+
52
+ #: `--no-ingest`ยท`--strict-ingest` ๋„์›€๋ง โ€” startยทresume ์ด ๊ฐ™์€ ๋œป์œผ๋กœ ์“ด๋‹ค(๊ณ„ํš L6 ยง2.2).
53
+ NO_INGEST_HELP = (
54
+ "run ์ด ๋๋‚œ ๋’ค hub ํ›…(๋ฒˆ๋“คยทhub ์ ์žฌ)์„ ํ†ต์งธ๋กœ ๊ฑด๋„ˆ๋›ด๋‹ค โ€” ๋˜์‚ด๋ฆฌ๋Š” ๋ช…๋ น์„ ํ•œ ์ค„๋กœ ์•Œ๋ ค ์ค€๋‹ค"
55
+ )
56
+ STRICT_INGEST_HELP = (
57
+ "hub ํ›… ๋‹ค๋ฆฌ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด ์ข…๋ฃŒ ์ฝ”๋“œ 3 (๊ธฐ๋ณธ์€ 0 โ€” run ๊ฒฐ๊ณผ๋Š” ๋””์Šคํฌ์— ์žˆ๋‹ค). "
58
+ "SOLAR_EVAL_STRICT_INGEST=1 ๊ณผ ๊ฐ™๋‹ค"
59
+ )
60
+
61
+ #: Experiment Hub ๋ฌถ์Œ ์ง€๋ฌธ์ด ์„ ํƒ์„ ๋ณด์ง€ ์•Š๋Š”๋‹ค๋Š” ๊ฒฝ๊ณ (๊ณ„ํš D19 ยท ยง11-2).
62
+ FILTERED_RUN_EXPERIMENT_HUB_WARNING = (
63
+ " โš  Experiment Hub ๋Š” ์•„์ง ํ•„ํ„ฐ run ์„ ๊ฐ™์€ ํƒœ์Šคํฌ ์ „์ฒด์…‹์˜ '๊ฐ™์€ ์„ค์ • ๋ฐ˜๋ณต' ๊ณผ ๊ตฌ๋ถ„ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค "
64
+ "(ยง11-2) โ€” ์ด run ์˜ ์ ์ˆ˜๋Š” ์œ„ CLI ์ถœ๋ ฅ๊ณผ note ๋กœ ์ฝ์œผ์„ธ์š”."
65
+ )
66
+
67
+ #: rubric judge ํƒœ์Šคํฌ์˜ `runs start` ๋’ค ์•ˆ๋‚ด(๊ณ„ํš 2026-09-28 L12 D-L12-6) โ€” in-run ์€ ๋ ˆ๊ฑฐ์‹œ ์ฑ„์ ๋งŒ, ์ฑ„์  ๋ฒˆ๋“ค์€ `runs eval` ์ด ๋‚ธ๋‹ค.
68
+ JUDGE_BUNDLE_HINT = "judge ์ฑ„์  ๋ฒˆ๋“ค์€ `solar-eval runs eval {project} -r {run}` ์ด ๋ฐœํ–‰ํ•œ๋‹ค"
69
+
70
+
71
  @runs_group.command("list")
72
  @click.argument("project")
73
  @click.option("--task", "-t", default=None, help="Filter by task name")
 
145
  @click.option(
146
  "--note",
147
  default=None,
148
+ help="์ด run ์„ ์™œ ๋Œ๋ฆฌ๋Š”์ง€ ํ•œ ์ค„ (run.json ์— ๊ธฐ๋ก๋˜๊ณ  Experiment Hub ๊ฐ€ ๊ทธ๋Œ€๋กœ ์ฝ๋Š”๋‹ค)",
149
  )
150
  @click.option(
151
  "--set",
 
157
  "์˜ˆ: --set steps.basic_correction.reasoning_effort=high. ์—ฌ๋Ÿฌ ๋ฒˆ ์“ธ ์ˆ˜ ์žˆ๋‹ค."
158
  ),
159
  )
160
+ @click.option("--no-ingest", is_flag=True, help=NO_INGEST_HELP)
161
+ @click.option("--strict-ingest", is_flag=True, help=STRICT_INGEST_HELP)
162
+ @click.option("--where", "where", multiple=True, metavar="KEY=V1,V2", help=WHERE_HELP)
 
 
163
  @click.pass_context
164
+ def start_run(ctx: click.Context, project: str, **options: Any) -> None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  """Start an evaluation run.
166
 
167
  Example:
 
173
  -p prompt_dev_v23 -m solar-pro3 -P pipeline_dev_v24 \\
174
  --set steps.basic_correction.repeat_temperatures='[0.0, 0.5]' \\
175
  --note "SC ์˜จ๋„ ํญ ์Šค์œ•"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
+ solar-eval runs start chosun-proofreading -t ci-v2-all \\
178
+ -p prompt_prod_251231 -m solar-pro3 --where tags=yul-ryul
 
 
179
  """
180
+ # click ์˜ต์…˜ ์ด๋ฆ„์ด `_start_local` ํ‚ค์›Œ๋“œ ์ธ์ž ์ด๋ฆ„๊ณผ ๊ฐ™๋‹ค โ€” ๊ทธ๋Œ€๋กœ ๋„˜๊ธด๋‹ค.
181
+ strict = strict_requested(options.pop("strict_ingest"))
182
+ result = _start_local(ctx.obj["config"], project=project, strict_ingest=strict, **options)
183
+ # ํ›… ์‹คํŒจ๋Š” ๊ธฐ๋ณธ ์ข…๋ฃŒ 0 โ€” strict ์—์„œ๋งŒ 3(D-L6-11 ยท ๋งˆ์Šคํ„ฐ D-M2-3).
184
+ code = exit_code_for(result, strict)
185
+ if code:
186
+ ctx.exit(code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
 
189
  def _build_run_name(task: str, pipeline_name: str, prompt_name: str) -> str:
 
204
  ๊ฐ™์€ ์ดˆ์— ๊ฐ™์€ (task, pipeline, prompt) ์กฐํ•ฉ์œผ๋กœ ๋‘ run ์ด ์‹œ์ž‘๋˜๋ฉด
205
  `_build_run_name` ์ด ๊ฐ™์€ ์ด๋ฆ„์„ ๋‚ธ๋‹ค. `mkdir(exist_ok=False)` ๋ฅผ ์“ฐ๋ฉด ๋‘ ํ”„๋กœ์„ธ์Šค๊ฐ€
206
  ๋™์‹œ์— ๊ฐ™์€ ์ด๋ฆ„์œผ๋กœ mkdir ํ•ด๋„ ์ •ํ™•ํžˆ ํ•˜๋‚˜๋งŒ ์„ฑ๊ณตํ•˜๊ณ , ์ง„ ์ชฝ์€ ์ƒˆ ํƒ€์ž„์Šคํƒฌํ”„๋กœ
207
+ ์žฌ์‹œ๋„ํ•œ๋‹ค โ€” ํŒŒ์ผ ํฌ๋งท ์ž์ฒด๋Š” ์•ˆ ๋ฐ”๊พธ๋ฏ€๋กœ Experiment Hub ์˜ run_id ํŒŒ์‹ฑ๊ณผ ํ˜ธํ™˜๋œ๋‹ค.
208
  """
209
  for _ in range(_RUN_DIR_MAX_ATTEMPTS):
210
  run_name = _build_run_name(task, pipeline_name, prompt_name)
 
217
  raise click.ClickException(f"Could not allocate a unique run directory under {artifacts_dir}")
218
 
219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  def _start_local(
221
  cfg,
222
+ *,
 
 
 
 
 
 
 
 
 
223
  note: str | None = None,
 
224
  no_ingest: bool = False,
225
+ strict_ingest: bool = False,
226
+ hook_options: HookOptions | None = None,
227
+ **plan_options: Any,
228
+ ) -> HookResult | None:
229
+ """run ํ•˜๋‚˜๋ฅผ ๋กœ์ปฌ์—์„œ ๋Œ๋ฆฐ๋‹ค (๊ณ„ํš ยง9 G2 ์ˆœ์„œ).
230
+
231
+ `plan_options` ๋Š” `run_meta.plan_run` ์˜ ํ‚ค์›Œ๋“œ ์ธ์ž ๊ทธ๋Œ€๋กœ๋‹ค โ€” `project`ยท`task`ยท`prompt_name`ยท
232
+ `model`ยท`pipeline_name`ยท`reasoning_effort`ยท`temperature`ยท`max_tokens`ยท`max_workers`ยท`limit`
233
+ (ํ•„์ˆ˜), `set_options`ยท`where`(์„ ํƒ). ๋ชจ๋ฅด๋Š” ํ‚ค๋Š” `plan_run` ์ด TypeError ๋กœ ๊ฑฐ๋ถ€ํ•œ๋‹ค.
234
+
235
+ `reject_defaults_where` โ†’ ์„ ํƒ(`--where`ยท`limit`) โ†’ `prepare_dataset` โ†’ `build_task_pipeline`
236
+ โ†’ run.json ๋ฉ”ํƒ€(`dataset`ยท`snapshot` + ์ž๊ธฐ ๊ฒ€์ฆ) โ†’ `_make_run_dir` โ†’ ๋Ÿฌ๋„ˆ์— ์ค€๋น„๋œ ๋ฐ์ดํ„ฐ์…‹ยท
237
+ ์„ ํƒยทํŒŒ์ดํ”„๋ผ์ธ **๊ฐ™์€ ์ธ์Šคํ„ด์Šค**๋ฅผ ๋„˜๊ธด๋‹ค. run.json ๋ฉ”ํƒ€๋Š” ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ธฐ ์ „์— ์กฐ๋ฆฝํ•œ๋‹ค โ€”
238
+ ์—†๋Š” ์‚ฌ์ „ยท๊นจ์ง„ lock ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋นˆ run ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋‚จ๊ธฐ์ง€ ์•Š๊ฒŒ.
239
+
240
+ ๋๋‚˜๋ฉด run ์ข…๋ฃŒ ํ›…(`hub_hook.run_hook`)์„ ๋ถ€๋ฅด๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋Œ๋ ค์ค€๋‹ค(hub ๋Œ€์ƒ์ด ์•„๋‹ˆ๋ฉด None ์ผ ์ˆ˜ ์žˆ๋‹ค).
241
+ ํ›…์€ ์‹คํŒจํ•ด๋„ ์˜ˆ์™ธ๋ฅผ ๋‚ด์ง€ ์•Š๋Š”๋‹ค โ€” ์ข…๋ฃŒ ์ฝ”๋“œ๋Š” ํ˜ธ์ถœ์ž๊ฐ€ `exit_code_for` ๋กœ ์ •ํ•œ๋‹ค.
242
+ `hook_options` ๋ฅผ ์ฃผ๋ฉด(batch โ€” `HookMode.BATCH_ITEM`) `no_ingest`ยท`strict_ingest` ๋Œ€์‹  ๊ทธ๊ฒƒ์„ ์“ด๋‹ค(์Šค๋ชจํฌ ํŒ์ •๋งŒ ๋ง์”Œ์šด๋‹ค).
243
+ """
244
+ from solar_eval.cli.commands import run_meta
245
  from solar_eval.stores import JsonlStore
246
 
247
+ plan = run_meta.plan_run(cfg, **plan_options)
248
+ project = plan.project
249
+ meta = run_meta.build_run_meta(cfg, plan, note)
250
+ # ์ด๋ฆ„ ์ถฉ๋Œ์€ ์—ฌ๊ธฐ์„œ ์›์ž์ ์œผ๋กœ ๊ฑธ๋Ÿฌ์ง„๋‹ค(F10). run.json ์€ ๊ทธ ์งํ›„ ํ•œ ๋ฒˆ์— ์“ด๋‹ค.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  run_name, run_dir = _make_run_dir(
252
+ cfg.artifacts_dir(project), plan.task, plan.pipeline_name, plan.prompt_name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  )
254
+ (run_dir / "run.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2))
255
+ _echo_run_info(plan, meta, f"{project}/{run_name}", run_dir)
256
+ execute_plan(plan, run_name, JsonlStore(run_dir))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  _warn_if_incomplete(run_dir)
259
+ if plan.selection is not None and plan.selection.where is not None:
260
+ click.secho(FILTERED_RUN_EXPERIMENT_HUB_WARNING, fg="yellow")
261
  click.echo(f"\nResults saved to: {run_dir}/")
262
+ if hook_options is None:
263
+ hook_options = HookOptions(command="start", no_ingest=no_ingest, strict=strict_ingest)
264
+ options = replace(hook_options, smoke=is_smoke_run(plan))
265
+ result = run_hook(cfg, project, run_dir, options)
266
+ _echo_judge_bundle_hint(plan, project, run_name)
267
+ return result
268
 
269
 
270
+ def _echo_judge_bundle_hint(plan, project: str, run_name: str) -> None:
271
+ """rubric judge ํƒœ์Šคํฌ๋ฉด ํ›… ๋’ค ์•ˆ๋‚ด ํ•œ ์ค„ โ€” ํ›…(eval-wrap)์€ lcs ์ „์šฉ์ด๋ผ judge ์ฑ„์  ๋ฒˆ๋“ค์„ ๋‚ด์ง€ ์•Š๋Š”๋‹ค(D-L12-6)."""
272
+ from solar_eval.evaluators.judge_config import RUBRIC_JUDGE_TYPE
273
+ from solar_eval.scoring_publish.publish import evaluator_type
274
 
275
+ if evaluator_type(plan.task_config.get("evaluator")) == RUBRIC_JUDGE_TYPE:
276
+ click.secho(f" {JUDGE_BUNDLE_HINT.format(project=project, run=run_name)}", fg="blue")
 
 
277
 
 
 
 
 
278
 
279
+ def _echo_run_info(plan, meta: dict, run_ref: str, run_dir: Path) -> None:
280
+ """run ์‹œ์ž‘ ์š”์•ฝ โ€” ์„ค์ •ยท์ง€๋ฌธ(config sha256:<12>)ยท์„ ํƒยท์žฌํ˜„ ๊ฐ€๋Šฅ์„ฑ."""
281
+ from solar_eval.cli.commands.run_meta import short_fingerprint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
+ snapshot = meta["snapshot"]
284
+ dataset = meta["dataset"]
285
+ click.secho(f"Prompt: {plan.prompt_name} [{plan.prompt_format}]", fg="blue")
286
+ click.secho(f"Model: {plan.model}", fg="blue")
287
+ click.secho(f"Pipeline: {plan.pipeline_name}", fg="blue")
288
+ for path, value in (plan.pipeline_overrides or {}).items():
289
+ click.secho(f" override: {path} = {value!r}", fg="magenta")
290
+ click.secho(
291
+ f"Fingerprint: config {short_fingerprint(snapshot['config_fingerprint'])}"
292
+ f" / prompt {meta['prompt_sha']} / pipeline {meta['pipeline_sha']}",
293
+ fg="blue",
294
+ )
295
+ if plan.reasoning_effort:
296
+ click.secho(f"Reasoning effort: {plan.reasoning_effort}", fg="blue")
297
+ selection = plan.selection
298
+ if selection is not None and selection.where is not None:
299
+ click.secho(f"Where: {selection.where.describe()}", fg="yellow")
300
+ if selection is not None and selection.limit:
301
+ click.secho(f"Limit: {selection.limit} samples", fg="yellow")
302
+ click.secho(f"Rows: {dataset['rows_selected']}/{dataset['rows_total']}", fg="blue")
303
+ _echo_reproducibility(snapshot)
304
+ click.secho(f"Starting local run: {run_ref}", fg="blue")
305
+ click.secho(f"Output: {run_dir}", fg="blue")
306
 
307
 
308
+ def _echo_reproducibility(snapshot: dict) -> None:
309
+ """์žฌํ˜„ ๊ฐ€๋Šฅ์„ฑ ํ•œ ์ค„ โ€” ๋ง‰๋Š” ์ด์œ ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ ์ค€๋‹ค(๊ณ„ํš R5)."""
310
+ repro = snapshot.get("reproducibility") or {}
311
+ verdict = repro.get("rerunnable")
312
+ label = {True: "true", False: "false", None: "unknown"}[verdict]
313
+ blockers = repro.get("rerunnable_blockers") or []
314
+ suffix = f" โ€” {', '.join(blockers)}" if blockers else ""
315
+ click.secho(f"Rerunnable: {label}{suffix}", fg="green" if verdict else "yellow")
316
+ if snapshot.get("missing"):
317
+ click.secho(f" snapshot missing: {', '.join(snapshot['missing'])}", fg="yellow")
318
 
319
 
320
  def _warn_if_incomplete(run_dir: Path) -> None:
 
348
  )
349
 
350
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  @runs_group.command("batch")
352
  @click.argument("project")
353
  @click.option("--task", "-t", required=True, help="Task name")
 
362
  )
363
  @click.option("--reasoning-effort", "-re", default=None, help="Reasoning effort")
364
  @click.option("--max-workers", "-w", default=5, type=int, help="Max parallel workers")
365
+ @click.option("--where", "where", multiple=True, metavar="KEY=V1,V2", help=WHERE_HELP)
366
+ @click.option("--no-ingest", is_flag=True, help=NO_INGEST_HELP)
367
+ @click.option("--strict-ingest", is_flag=True, help=STRICT_INGEST_HELP)
368
  @click.pass_context
369
+ def batch_runs(ctx, project, task, prompts, no_ingest, strict_ingest, **run_options):
370
+ """Run evaluation for multiple prompts sequentially.
371
+
372
+ run ๋งˆ๋‹ค ๋ฒˆ๋“ค ๋‹ค๋ฆฌ๋งŒ ๋Œ๊ณ (`HookMode.BATCH_ITEM`), ๋์— hub ๋‹ค๋ฆฌ๋ฅผ ํ•œ ๋ฒˆ ๋ถ€๋ฅธ๋‹ค(๊ณ„ํš L6 D-L6-12).
373
+ ์ข…๋ฃŒ ์ฝ”๋“œ: ํ”„๋กฌํ”„ํŠธ ์‹คํŒจ๊ฐ€ ์žˆ์œผ๋ฉด 1(๋จผ์ €), ์•„๋‹ˆ๊ณ  strict ์ด๋ฉฐ ํ›…์ด ์‹คํŒจํ•œ run ์ด ์žˆ์œผ๋ฉด 3, ๊ทธ ๋ฐ– 0(D-L6-11).
374
+ """
375
  cfg = ctx.obj["config"]
376
  if cfg.is_remote:
377
  raise click.ClickException("Batch is local-mode only")
378
 
379
  prompt_list = [p.strip() for p in prompts.split(",")]
380
+ # ํ”„๋กฌํ”„ํŠธ์™€ ๋ฌด๊ด€ํ•œ ๊ฒ€์‚ฌ(defaults.whereยท--where ๋ฌธ๋ฒ•ยทํƒœ์Šคํฌ where ํ•ฉ์น˜๊ธฐ)๋Š” ๋ฃจํ”„ ์ „์— ํ•œ ๋ฒˆ โ€”
381
+ # ํ‹€๋ ธ์œผ๋ฉด ํ”„๋กฌํ”„ํŠธ๋งˆ๋‹ค ๊ฐ™์€ ์˜ค๋ฅ˜๋ฅผ ๋˜ํ’€์ดํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ๋ฉˆ์ถ˜๋‹ค(D3 "์‹คํŒจ๋Š” ํฌ๊ฒŒ").
382
+ project_config, task_config = find_task(cfg, project, task)
383
+ start_options(project_config, task_config, (), run_options["where"], None)
384
+ preflight_judge_config(task_config)
385
  click.secho(f"Batch run: {project}/{task} โ€” prompts: {prompt_list}", fg="blue", bold=True)
386
 
387
+ options = HookOptions(
388
+ command=COMMAND_BATCH,
389
+ mode=HookMode.BATCH_ITEM,
390
+ no_ingest=no_ingest,
391
+ strict=strict_requested(strict_ingest),
392
+ )
393
+ results, failed = _run_batch_prompts(cfg, project, task, prompt_list, options, run_options)
394
+ results = _with_batch_hub_leg(results, finish_batch(cfg, project, results, options))
395
+ click.secho(f"\n{'=' * 50}", fg="blue")
396
+ # standalone(๋ ˆํฌ ๋ฐ– โ€” ํ›…์ด None ๋งŒ ๋Œ๋ ค์ค€๋‹ค)์€ ํ›… ์ฝ˜์†”์ด ์—†๋‹ค(ยง2.3 "์˜ค๋Š˜๊ณผ ๊ฐ™๋‹ค", ๋ฆฌ๋ทฐ ์ง€์  3).
397
+ if cfg.repo_root is not None or any(r is not None for r in results):
398
+ echo_batch_summary(results)
399
+ if failed:
400
+ raise click.ClickException(
401
+ f"{len(failed)} of {len(prompt_list)} prompts failed: {', '.join(failed)}"
402
+ )
403
+ code = max((exit_code_for(r, options.strict) for r in results), default=0)
404
+ if code:
405
+ ctx.exit(code)
406
+ click.secho("Batch complete!", fg="green", bold=True)
407
+
408
+
409
+ def _run_batch_prompts(
410
+ cfg, project: str, task: str, prompt_list: list[str], options: HookOptions, run_options: dict
411
+ ) -> tuple[list[HookResult | None], list[str]]:
412
+ """ํ”„๋กฌํ”„ํŠธ๋งˆ๋‹ค run ํ•˜๋‚˜ + ๋ฒˆ๋“ค ๋‹ค๋ฆฌ. ํ›… ๊ฒฐ๊ณผ์™€ ์‹คํŒจํ•œ ํ”„๋กฌํ”„ํŠธ ์ด๋ฆ„์„ ๋Œ๋ ค์ค€๋‹ค.
413
+
414
+ ํ›…์€ ์˜ˆ์™ธ๋ฅผ ๋‚ด์ง€ ์•Š๋Š”๋‹ค(`run_hook` ์ด ๋ด‰์‡„ํ•œ๋‹ค, D-L6-2) โ€” ์—ฌ๊ธฐ์„œ ์žกํžˆ๋Š” ์˜ˆ์™ธ๋Š” run ์ž์ฒด์˜ ์‹คํŒจ๋ฟ์ด๋‹ค.
415
+ """
416
+ results: list[HookResult | None] = []
417
+ failed: list[str] = []
418
  for pname in prompt_list:
419
  click.secho(f"\n{'=' * 50}", fg="blue")
420
  click.secho(f"Running prompt: {pname}...", fg="blue", bold=True)
421
  click.secho(f"{'=' * 50}", fg="blue")
422
  try:
423
+ results.append(
424
+ _start_local(
425
+ cfg,
426
+ project=project,
427
+ task=task,
428
+ prompt_name=pname,
429
+ temperature=0.0,
430
+ max_tokens=4000,
431
+ limit=None,
432
+ hook_options=options,
433
+ **run_options,
434
+ )
435
  )
436
  except Exception as e:
437
+ # ํ•œ ํ”„๋กฌํ”„ํŠธ์˜ ์‹คํŒจ๊ฐ€ ๋‚˜๋จธ์ง€๋ฅผ ๋ง‰์ง€ ์•Š๊ฒŒ ์ด์–ด ๊ฐ€๋˜, ๋์—์„œ exit code ๋กœ ์•Œ๋ฆฐ๋‹ค.
438
  click.secho(f" {pname} failed: {e}", fg="red")
439
+ failed.append(pname)
440
+ return results, failed
 
441
 
442
 
443
+ def _with_batch_hub_leg(
444
+ results: list[HookResult | None], hub: LegResult | None
445
+ ) -> list[HookResult | None]:
446
+ """batch ๋ hub ๋‹ค๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ run ๋ณ„ ๊ฒฐ๊ณผ์— ์–น๋Š”๋‹ค(์š”์•ฝยท์ข…๋ฃŒ ์ฝ”๋“œ์šฉ โ€” ์ƒํƒœ ํŒŒ์ผ์€ `finish_batch` ๊ฐ€ ๊ณ ์นœ๋‹ค).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
+ ๋ฒˆ๋“ค์ด ์ƒ๊ธด run ์€ ๋ ๋‹ค๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ, ๋ฒˆ๋“ค์ด ์—†๋Š” run ์€ `skipped:no_bundle` ์„ ๋ฐ›๋Š”๋‹ค(`hook._legs_in_lock` ๊ณผ ๊ฐ™์€ ๊ทœ์น™).
449
+ """
450
+ merged: list[HookResult | None] = []
451
+ for result in results:
452
+ pending = result.legs.get(LEG_HUB) if result is not None else None
453
+ if result is None or pending is None or pending.code != BATCH_PENDING:
454
+ merged.append(result)
455
+ continue
456
+ leg = hub if hub is not None and bundle_ready(result.legs.get(LEG_BUNDLE)) else None
457
+ leg = leg or LegResult.skipped(NO_BUNDLE)
458
+ merged.append(replace(result, legs={**result.legs, LEG_HUB: leg}))
459
+ return merged
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
 
462
  @runs_group.command("results")
 
496
  results = results[:limit]
497
  click.secho(format_results_table(results), fg="blue")
498
  click.echo(f"\nShowing {len(results)} result(s)")
499
+
500
+
501
+ # ๋–ผ์–ด ๋‚ธ ๋ช…๋ น ๋“ฑ๋ก โ€” `solar-eval runs resume|eval` ํ‘œ๋ฉด์€ ๊ทธ๋Œ€๋กœ๋‹ค.
502
+ runs_group.add_command(resume_run)
503
+ runs_group.add_command(eval_run)
solar_eval/cli/commands/score.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """`solar-eval score` โ€” ์˜คํ”„๋ผ์ธ ์žฌ์ฑ„์  ๋ช…๋ น ๊ทธ๋ฃน (๊ณ„ํš 2026-09-26 L3b ยง2.1).
2
+
3
+ `score proofread-diff` ๋Š” run ๋ฒˆ๋“ค์˜ `results.jsonl` ์„ ๋ ˆ๊ฑฐ์‹œ `lcs_diff` ์™€ ๊ฐ™์€ ํ•จ์ˆ˜๋กœ ๋‹ค์‹œ ์ฑ„์ ํ•ด ์ฑ„์  ๋ฒˆ๋“ค
4
+ (`poc-scoring/v1`, `purpose=rescore_after_fix`)๋กœ ๋‚ธ๋‹ค. ๋‘ ๋ชจ๋“œ:
5
+
6
+ - `<run_bundle_dir>โ€ฆ` ๋ชจ๋“œ: ๋ฒˆ๋“ค์ด `โ€ฆ/eval/bundles/<id>` ์— ์žˆ์œผ๋ฉด ๊ธฐ๋ณธ ์ถœ๋ ฅ์€ `โ€ฆ/eval/evaluations`, ์•„๋‹ˆ๋ฉด `--out` ํ•„์ˆ˜.
7
+ `--mirror` ๋ฉด ๊ฐ ๋ฒˆ๋“ค ๋งค๋‹ˆํŽ˜์ŠคํŠธ `case` ์˜ ๋ฏธ๋Ÿฌ `evaluations` ์— ๋†“๋Š”๋‹ค.
8
+ - `--case a/c --all` ๋ชจ๋“œ: ๋ฏธ๋Ÿฌ์˜ ๊ทธ case run ๋ฒˆ๋“ค ์ „๋ถ€(`poc_bundle.adopt` ๋กœ ์ฑ„ํƒ). `--mirror` ๋˜๋Š” `--out` ์ด ํ•„์š”ํ•˜๋‹ค.
9
+ `--run <run_id>`(๋ฐ˜๋ณต)๋ฅผ ์ฃผ๋ฉด ๊ทธ run ๋“ค๋งŒ(๊ณ„ํš 2026-09-27 L6 ยง2.11 โ€” ์ฑ„ํƒ ๊ทœ์น™์€ ๊ฐ™๊ณ , ์˜์—ญ์— ์—†๋Š” run ์€ ์˜ค๋ฅ˜
10
+ `run_not_in_mirror`). `--run` ์€ ์ด ๋ชจ๋“œ์—์„œ๋งŒ ๋ฐ›๊ณ , ๋นˆ ๊ฐ’ยท๊ณต๋ฐฑ๋งŒยท์ œ์–ด ๋ฌธ์žยท512์ž ์ดˆ๊ณผ๋Š” ์‚ฌ์šฉ ์˜ค๋ฅ˜๋‹ค(ES
11
+ `hub ingest --expect-run` ๊ณผ ๊ฐ™์€ ๋ฒ”์œ„ โ€” ์—†๋Š” run ์ด stderr ์— ๊ทธ๋Œ€๋กœ ์ฐํžˆ๋ฏ€๋กœ ์ค„ ์œ„์กฐ๋ฅผ ๋ง‰๋Š”๋‹ค).
12
+ - `--out DIR` ๊ณผ `--mirror` ๋Š” ํ•จ๊ป˜ ์“ฐ์ง€ ์•Š๋Š”๋‹ค. `--mirror-root` ๋Š” ๋ฏธ๋Ÿฌ **๋ฒ„ํ‚ท์˜ ๋ถ€๋ชจ**๋‹ค(ยง2.1 "๋ฏธ๋Ÿฌ ๋ฃจํŠธ ์ธต").
13
+ ๋””๋ ‰ํ† ๋ฆฌ ๋ชจ๋“œ์—์„œ๋Š” `--mirror` ์™€ ํ•จ๊ป˜๋งŒ ๋ฐ›๋Š”๋‹ค. ์—†๋Š” run ๋ฒˆ๋“ค ๊ฒฝ๋กœ๋Š” ์‚ฌ์šฉ ์˜ค๋ฅ˜(์ข…๋ฃŒ 2)๋‹ค.
14
+
15
+ stdout ์€ run ๋งˆ๋‹ค ํ•œ ์ค„ `written <path>` ยท `exists <path>` ยท `skip <run_id> <code>` ์ด๊ณ , case ๋ชจ๋“œ๋Š” ๋งˆ์ง€๋ง‰ ์ค„์— JSON ์š”์•ฝ
16
+ `{written, exists, skipped:{code:n}, errors, n_original_missing}` ์„ ๋‚ธ๋‹ค. ์˜ค๋ฅ˜๋Š” stderr ์— `error <run_id> <code>` ํ•œ ์ค„์”ฉ
17
+ (case ์ „์ฒด ์˜ค๋ฅ˜๋Š” run_id ์ž๋ฆฌ์— `-`, `case_area_missing`ยท`case_area_empty` ๋Š” ๋’ค์— ํ•ด์„ํ•œ ์˜์—ญ ๊ฒฝ๋กœ๋ฅผ ๋ถ™์ธ๋‹ค).
18
+ ์–ด๋А ์ชฝ์—๋„ ์ž…๋ ฅยท์ถœ๋ ฅยท์ •๋‹ต ์›๋ฌธ์€ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค.
19
+
20
+ ์ข…๋ฃŒ ์ฝ”๋“œ: 0 = ์ „๋ถ€ written/exists/๊ทœ์น™ skip ยท 1 = ์˜ค๋ฅ˜ 1๊ฑด ์ด์ƒ ยท 2 = ์‚ฌ์šฉ ์˜ค๋ฅ˜.
21
+
22
+ import ๊ฒฝ๊ณ„(D1): ์ด ๋ชจ๋“ˆ์€ click ๋งŒ ๋ชจ๋“ˆ ์ตœ์ƒ์œ„์—์„œ import ํ•œ๋‹ค. `solar_eval.rescore`(๊ทธ๋ฆฌ๊ณ  ๊ทธ๊ฒƒ์ด ๋Œ์–ด์˜ค๋Š”
23
+ `poc_bundle`)๋Š” **๋ช…๋ น ํ•จ์ˆ˜ ์•ˆ์—์„œ** import ํ•œ๋‹ค โ€” `solar-eval` CLI ๋ฅผ ๋„์šฐ๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์žฌ์ฑ„์ ๊ธฐยท๋ฒˆ๋“ค writer ๊ฐ€
24
+ ๋กœ๋”ฉ๋˜์ง€ ์•Š๊ฒŒ.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import sys
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ import click
35
+
36
+ _DIR = click.Path(file_okay=False, path_type=Path)
37
+ # ์ž…๋ ฅ run ๋ฒˆ๋“ค์€ ์žˆ์–ด์•ผ ํ•œ๋‹ค โ€” ๊ฒฝ๋กœ ์˜คํƒ€๊ฐ€ ๊ทœ์น™ skip(`latest_bundle_invalid`)ยท์ข…๋ฃŒ 0 ์œผ๋กœ ๋๋‚˜์ง€ ์•Š๊ฒŒ click ์ด ์ข…๋ฃŒ 2 ๋กœ ๋ง‰๋Š”๋‹ค.
38
+ _EXISTING_DIR = click.Path(exists=True, file_okay=False, path_type=Path)
39
+ EXIT_ERRORS = 1
40
+ EXIT_USAGE = 2
41
+
42
+
43
+ @click.group("score")
44
+ def score_group() -> None:
45
+ """Offline rescoring of run bundles (scoring bundles, poc-scoring/v1)."""
46
+
47
+
48
+ @score_group.command("proofread-diff")
49
+ @click.argument("run_bundle_dirs", nargs=-1, type=_EXISTING_DIR)
50
+ @click.option("--case", "case_key", default=None, help="accounts/<a>/<c> as a/c (with --all).")
51
+ @click.option("--all", "all_runs", is_flag=True, default=False, help="Every adopted run bundle.")
52
+ @click.option("--out", "out_dir", type=_DIR, default=None, help="Parent dir for scoring bundles.")
53
+ @click.option("--mirror", is_flag=True, default=False, help="Write into the local mirror.")
54
+ @click.option(
55
+ "--mirror-root",
56
+ type=_DIR,
57
+ default=None,
58
+ help="Parent of the mirror bucket (default: data home blob-mirror).",
59
+ )
60
+ @click.option(
61
+ "--run",
62
+ "run_ids",
63
+ multiple=True,
64
+ help="Only this run_id (repeatable; with --case --all).",
65
+ )
66
+ def proofread_diff(
67
+ run_bundle_dirs: tuple[Path, ...],
68
+ case_key: str | None,
69
+ all_runs: bool,
70
+ out_dir: Path | None,
71
+ mirror: bool,
72
+ mirror_root: Path | None,
73
+ run_ids: tuple[str, ...],
74
+ ) -> None:
75
+ """Rescore run bundles with proofread_diff@2 (same scoring as legacy lcs_diff)."""
76
+ _check_usage(run_bundle_dirs, case_key, all_runs, out_dir, mirror, mirror_root, run_ids)
77
+ if case_key is not None:
78
+ code = _run_case(case_key, out_dir, mirror_root, frozenset(run_ids) or None)
79
+ else:
80
+ code = _run_dirs(run_bundle_dirs, out_dir, mirror_root if mirror else None, mirror)
81
+ sys.exit(code)
82
+
83
+
84
+ def _usage(message: str) -> None:
85
+ click.echo(f"usage error: {message}", err=True)
86
+ sys.exit(EXIT_USAGE)
87
+
88
+
89
+ def _check_usage(
90
+ dirs: tuple[Path, ...],
91
+ case_key: str | None,
92
+ all_runs: bool,
93
+ out_dir: Path | None,
94
+ mirror: bool,
95
+ mirror_root: Path | None,
96
+ run_ids: tuple[str, ...] = (),
97
+ ) -> None:
98
+ """๋‘ ๋ชจ๋“œ์˜ ์ธ์ž ์กฐํ•ฉ โ€” ์–ด๊ธ‹๋‚˜๋ฉด ์ข…๋ฃŒ 2."""
99
+ if out_dir is not None and mirror:
100
+ _usage("--out and --mirror are mutually exclusive")
101
+ if case_key is not None or all_runs:
102
+ if dirs:
103
+ _usage("give run bundle directories or --case/--all, not both")
104
+ if case_key is None or not all_runs:
105
+ _usage("--case a/c needs --all (and --all needs --case)")
106
+ if out_dir is None and not mirror:
107
+ _usage("--case --all needs --mirror or --out DIR")
108
+ elif not dirs:
109
+ _usage("give run bundle directories, or --case a/c --all")
110
+ elif mirror_root is not None and not mirror:
111
+ # ๋””๋ ‰ํ† ๋ฆฌ ๋ชจ๋“œ์—์„œ --mirror-root ๋Š” --mirror ์˜ ์ถœ๋ ฅ ์ž๋ฆฌ์ผ ๋ฟ์ด๋‹ค โ€” ์กฐ์šฉํžˆ ๋ฒ„๋ฆฌ์ง€ ์•Š๋Š”๋‹ค
112
+ _usage("--mirror-root needs --mirror when rescoring run bundle directories")
113
+ if run_ids:
114
+ _check_run_filter(case_key, run_ids)
115
+
116
+
117
+ def _check_run_filter(case_key: str | None, run_ids: tuple[str, ...]) -> None:
118
+ """`--run` ์€ case ๋ชจ๋“œ์˜ run ํ•„ํ„ฐ๋‹ค(L6 ยง2.11) โ€” ๋””๋ ‰ํ† ๋ฆฌ ๋ชจ๋“œ์—์„œ ์กฐ์šฉํžˆ ๋ฒ„๋ฆฌ์ง€ ์•Š๊ณ , ๊ฐ’์€ ๊ฒฝ๊ณ„์—์„œ ๊ฑฐ๋ฅธ๋‹ค.
119
+
120
+ case ๋ชจ๋“œ์˜ `--case`/`--all` ์ง์€ `_check_usage` ๊ฐ€ ๋จผ์ € ๋ง‰์œผ๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ `case_key is None` ์ด๋ฉด ๋””๋ ‰ํ† ๋ฆฌ ๋ชจ๋“œ๋‹ค.
121
+ ๊ฐ’ ๊ทœ์น™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๊ฒฝ๊ณ„(`rescore_case`)์™€ ๊ฐ™์€ `is_valid_run_id` ๋‹ค. ๊ฑฐ๋ถ€ ๋ฌธ๊ตฌ์— ๊ฐ’ ์›๋ฌธ์€ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค.
122
+ """
123
+ from solar_eval.rescore.case import MAX_RUN_ID_LENGTH, is_valid_run_id
124
+
125
+ if case_key is None:
126
+ _usage("--run needs --case a/c --all (not run bundle directories)")
127
+ if not all(is_valid_run_id(r) for r in run_ids):
128
+ _usage(
129
+ "--run needs a non-blank run_id without control characters,"
130
+ f" at most {MAX_RUN_ID_LENGTH} characters"
131
+ )
132
+
133
+
134
+ def _run_case(
135
+ case_key: str,
136
+ out_dir: Path | None,
137
+ mirror_root: Path | None,
138
+ run_ids: frozenset[str] | None = None,
139
+ ) -> int:
140
+ from solar_eval.rescore.case import rescore_case
141
+ from solar_eval.rescore.paths import RescorePathError, parse_case_key
142
+
143
+ try:
144
+ account, case = parse_case_key(case_key)
145
+ except RescorePathError as exc:
146
+ _usage(str(exc))
147
+ summary = rescore_case(account, case, mirror_root=mirror_root, out_dir=out_dir, run_ids=run_ids)
148
+ for outcome in summary.outcomes:
149
+ _echo_outcome(outcome)
150
+ for failure in summary.failures:
151
+ suffix = f" {failure.detail}" if failure.detail else ""
152
+ click.echo(f"error {failure.run_id or '-'} {failure.code}{suffix}", err=True)
153
+ report: dict[str, Any] = summary.to_json()
154
+ click.echo(json.dumps(report, ensure_ascii=False, sort_keys=False))
155
+ return EXIT_ERRORS if report["errors"] else 0
156
+
157
+
158
+ def _run_dirs(
159
+ dirs: tuple[Path, ...], out_dir: Path | None, mirror_root: Path | None, mirror: bool
160
+ ) -> int:
161
+ from solar_eval.rescore.evaluator_doc import current_evaluator_document
162
+ from solar_eval.rescore.from_bundle import RescoreError, rescore_run_bundle
163
+ from solar_eval.rescore.paths import default_evaluations_dir, resolve_mirror_root
164
+
165
+ targets = [(d, out_dir or (None if mirror else default_evaluations_dir(d))) for d in dirs]
166
+ missing = [d for d, target in targets if target is None and not mirror]
167
+ if missing:
168
+ _usage(f"{missing[0]} is not under โ€ฆ/eval/bundles/ โ€” give --out DIR")
169
+ root = resolve_mirror_root(mirror_root) if mirror else None
170
+ evaluator = current_evaluator_document()
171
+ errors = 0
172
+ for bundle_dir, target in targets:
173
+ try:
174
+ outcome = rescore_run_bundle(bundle_dir, target, mirror_root=root, evaluator=evaluator)
175
+ except RescoreError as exc:
176
+ click.echo(f"error {exc.run_id or bundle_dir.name} {exc.code}", err=True)
177
+ errors += 1
178
+ continue
179
+ _echo_outcome(outcome, fallback=bundle_dir.name)
180
+ return EXIT_ERRORS if errors else 0
181
+
182
+
183
+ def _echo_outcome(outcome: Any, fallback: str = "-") -> None:
184
+ if outcome.status == "skip":
185
+ click.echo(f"skip {outcome.run_id or fallback} {outcome.code}")
186
+ else:
187
+ click.echo(f"{outcome.status} {outcome.path}")
solar_eval/cli/commands/sync.py CHANGED
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
17
  @click.option(
18
  "--mongo-uri",
19
  envvar="MONGODB_URI",
20
- default="mongodb://localhost:27017",
21
  help="MongoDB connection URI",
22
  )
23
  @click.option("--db", "database", default="solar_eval", help="MongoDB database name")
@@ -122,9 +122,10 @@ def _sync_single_run(store, project: str, run_dir: Path, cfg) -> dict[str, int]:
122
  "input": result.get("input", {}),
123
  "output": result.get("output", ""),
124
  "golden": result.get("golden", ""),
125
- "input_tokens": result.get("input_tokens", 0),
126
- "output_tokens": result.get("output_tokens", 0),
127
- "inference_time_ms": result.get("inference_time_ms", 0),
 
128
  }
129
  )
130
  sample_count = store.bulk_insert_samples(samples)
 
17
  @click.option(
18
  "--mongo-uri",
19
  envvar="MONGODB_URI",
20
+ default="mongodb://localhost:57017",
21
  help="MongoDB connection URI",
22
  )
23
  @click.option("--db", "database", default="solar_eval", help="MongoDB database name")
 
122
  "input": result.get("input", {}),
123
  "output": result.get("output", ""),
124
  "golden": result.get("golden", ""),
125
+ # ์‚ฌ์šฉ๋Ÿ‰ยท์ง€์—ฐ์ด ์—†๋Š” ํ–‰์€ None ๊ทธ๋Œ€๋กœ -- 0 ์„ ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค(L6b B1).
126
+ "input_tokens": result.get("input_tokens"),
127
+ "output_tokens": result.get("output_tokens"),
128
+ "inference_time_ms": result.get("inference_time_ms"),
129
  }
130
  )
131
  sample_count = store.bulk_insert_samples(samples)
solar_eval/cli/config.py CHANGED
@@ -1,28 +1,155 @@
1
- """CLI configuration from YAML file and environment variables."""
2
 
 
 
 
 
 
 
 
3
  import logging
4
  import os
 
 
 
5
  from pathlib import Path
6
  from typing import Any
7
 
8
  import yaml
9
 
10
- DEFAULT_CONFIG_PATH = Path.home() / ".upstage-cloud-dev" / "solar-eval-config.yaml"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  def default_projects_dir() -> Path:
16
  """CBS ๊ณต์šฉ ๋ฐ์ดํ„ฐ ํ™ˆ ์•„๋ž˜์˜ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ (huggingface ์บ์‹œ์™€ ๊ฐ™์€ ๋ฐฉ์‹).
17
 
18
  - ๊ธฐ๋ณธ๊ฐ’: ``~/.cache/cbs-data/solar-eval-projects``
19
  - ``CBS_DATA_HOME`` ์ด ์„ค์ •๋˜๋ฉด ๋ฐ์ดํ„ฐ ํ™ˆ ์ž์ฒด๋ฅผ ์˜ฎ๊ธด๋‹ค: ``$CBS_DATA_HOME/solar-eval-projects``
20
  - ``XDG_CACHE_HOME`` ์ด ์„ค์ •๋˜๋ฉด ์บ์‹œ ๋ฃจํŠธ๋กœ ์กด์ค‘ํ•œ๋‹ค: ``$XDG_CACHE_HOME/cbs-data/solar-eval-projects``
 
 
21
  """
22
  data_home = os.environ.get("CBS_DATA_HOME")
23
  if data_home:
24
  return Path(data_home).expanduser() / "solar-eval-projects"
25
- cache_root = os.environ.get("XDG_CACHE_HOME", "~/.cache")
26
  return Path(cache_root).expanduser() / "cbs-data" / "solar-eval-projects"
27
 
28
 
@@ -66,7 +193,7 @@ def discover_config_dirs(repo_root: Path | None) -> dict[str, Path]:
66
 
67
  def _load_yaml_config(config_path: Path | None = None) -> dict[str, Any]:
68
  """YAML ์„ค์ • ํŒŒ์ผ ๋กœ๋“œ. ํŒŒ์ผ ์—†์œผ๋ฉด ๋นˆ dict ๋ฐ˜ํ™˜."""
69
- path = config_path or Path(os.environ.get("SOLAR_EVAL_CONFIG", str(DEFAULT_CONFIG_PATH)))
70
  if not path.is_file():
71
  return {}
72
  try:
@@ -74,7 +201,8 @@ def _load_yaml_config(config_path: Path | None = None) -> dict[str, Any]:
74
  data = yaml.safe_load(f)
75
  return data if isinstance(data, dict) else {}
76
  except Exception as e:
77
- logger.warning("Failed to load config from %s: %s", path, e)
 
78
  return {}
79
 
80
 
@@ -83,13 +211,16 @@ class CLIConfig:
83
 
84
  Priority (low โ†’ high):
85
  1. Hard-coded defaults (projects_dir: ~/.cache/cbs-data/solar-eval-projects)
86
- 2. YAML config file (~/.upstage-cloud-dev/solar-eval-config.yaml)
87
- 3. Environment variables (SOLAR_EVAL_URL, CBS_DATA_HOME, SOLAR_EVAL_PROJECTS, etc.)
88
  4. CLI options (--remote, etc.)
89
  """
90
 
91
  def __init__(self, config_path: Path | None = None) -> None:
92
- yaml_cfg = _load_yaml_config(config_path)
 
 
 
93
 
94
  yaml_remote = yaml_cfg.get("remote_url") or yaml_cfg.get("remote")
95
  self.remote_url: str | None = os.environ.get("SOLAR_EVAL_URL", yaml_remote)
@@ -126,6 +257,10 @@ class CLIConfig:
126
  if openai_cfg.get("api_key") and not os.environ.get("OPENAI_API_KEY"):
127
  os.environ["OPENAI_API_KEY"] = openai_cfg["api_key"]
128
 
 
 
 
 
129
  @property
130
  def is_remote(self) -> bool:
131
  return self.remote_url is not None
 
1
+ """CLI configuration from YAML file and environment variables.
2
 
3
+ **`hub:` ์ ˆ(๊ณ„ํš 2026-09-28 L10 ยง2.6 ยท D-L10-6).** ์‚ฌ์šฉ์ž ์„ค์ • ํŒŒ์ผ์˜ `hub.database_url`ยท`hub.confirm` ์€ run ์ข…๋ฃŒ ํ›…์˜ hub
4
+ ๋‹ค๋ฆฌ opt-in ์ด๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ๋ชจ์–‘๋งŒ ์ฝ์–ด `CLIConfig.hub`(`HubConfig`)์— ๋‘๊ณ , ํ™˜๊ฒฝ ๋ณ€์ˆ˜์™€์˜ ์šฐ์„ ์ˆœ์œ„ยท์—์ด์ „ํŠธ ์„ธ์…˜ ํŒ์ •์€
5
+ `solar_eval.hub_hook.optin.resolve_hub_optin` ํ•œ ๊ณณ์ด ํ•œ๋‹ค. ๋ชจ์–‘ ์˜ค๋ฅ˜(๋งคํ•‘์ด ์•„๋‹˜ยทURL ๋ชจ์–‘์ด ์•„๋‹˜ยท๋ชจ๋ฅด๋Š” ํ‚คยท์„ค์ • ํŒŒ์ผ์ด
6
+ ์†Œ์œ ์ž ์ „์šฉ ๊ถŒํ•œ์ด ์•„๋‹˜)๋Š” `problems` ์— **ํ‚ค ์ด๋ฆ„๋งŒ** ์‹ฃ๋Š”๋‹ค โ€” ๊ฐ’์€ ๋กœ๊ทธยท์˜ˆ์™ธยท`repr` ์–ด๋””์—๋„ ์—†๋‹ค.
7
+ """
8
+
9
+ import functools
10
  import logging
11
  import os
12
+ import stat
13
+ from collections.abc import Mapping
14
+ from dataclasses import dataclass, field
15
  from pathlib import Path
16
  from typing import Any
17
 
18
  import yaml
19
 
20
+ #: 2026-10-02 ์ „ ์œ„์น˜ โ€” ์ƒˆ ๊ฒฝ๋กœ์— ํŒŒ์ผ์ด ์—†์„ ๋•Œ๋งŒ ๊ฒฝ๊ณ ์™€ ํ•จ๊ป˜ ์ฝ๋Š”๋‹ค(`make dev-docker-up` ์ด ์ƒˆ ๊ฒฝ๋กœ๋กœ ๋ณต์‚ฌํ•œ๋‹ค).
21
+ LEGACY_CONFIG_PATH = Path.home() / ".upstage-cloud-dev" / "solar-eval-config.yaml"
22
+
23
+
24
+ def _config_home() -> Path:
25
+ """๋ ˆํฌ ์‚ฌ์šฉ์ž ์„ค์ • ํ™ˆ โ€” `CBS_ARSENAL_CONFIG_HOME` > `$XDG_CONFIG_HOME/cbs-arsenal` > `~/.config/cbs-arsenal`.
26
+
27
+ Go `internal/config.ConfigHome` ๊ณผ ๊ฐ™์€ ๊ทœ์น™์ด๋‹ค(monorepo-conventions.md "์‚ฌ์šฉ์ž ์„ค์ • ํ™ˆ").
28
+ """
29
+ override = os.environ.get("CBS_ARSENAL_CONFIG_HOME")
30
+ if override:
31
+ return Path(override)
32
+ xdg = os.environ.get("XDG_CONFIG_HOME")
33
+ return Path(xdg) / "cbs-arsenal" if xdg else Path.home() / ".config" / "cbs-arsenal"
34
+
35
+
36
+ DEFAULT_CONFIG_PATH = _config_home() / "solar-eval.yaml"
37
+
38
+ #: `hub:` ์ ˆ์ด ๋ฐ›๋Š” ํ‚ค.
39
+ HUB_CONFIG_KEYS = ("database_url", "confirm")
40
+ #: `hub.database_url` ์ด ๋ฐ›๋Š” URL ๋ชจ์–‘(hub CLI `connect.ts` ์™€ ๊ฐ™์€ ๋‘ ์Šคํ‚ด).
41
+ HUB_URL_SCHEMES = ("postgres://", "postgresql://")
42
+ #: ๊ทธ๋ฃนยท๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ๊ถŒํ•œ ๋น„ํŠธ โ€” ์ž๊ฒฉ์ฆ๋ช… URL ์„ ๋‹ด์€ ์„ค์ • ํŒŒ์ผ์€ ์†Œ์œ ์ž๋งŒ ์ฝ๊ณ  ์จ์•ผ ํ•œ๋‹ค(ssh ํ‚ค์™€ ๊ฐ™์€ ๊ทœ์น™).
43
+ LOOSE_MODE_BITS = stat.S_IRWXG | stat.S_IRWXO
44
+ #: ๋ฌธ์ œ ๋ชฉ๋ก์— ์‹ฃ๋Š” ํ‚ค ์ด๋ฆ„ ๊ธธ์ด ์ƒํ•œ(๊ฐ’์ด ์•„๋‹ˆ๋ผ ์ด๋ฆ„์ด์ง€๋งŒ ๊ธธ๊ฒŒ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค).
45
+ _KEY_NAME_MAX = 40
46
 
47
  logger = logging.getLogger(__name__)
48
 
49
 
50
+ @dataclass(frozen=True)
51
+ class HubConfig:
52
+ """์„ค์ • ํŒŒ์ผ `hub:` ์ ˆ โ€” ๊ฐ’์€ `repr` ์— ์‹ฃ์ง€ ์•Š๋Š”๋‹ค. `problems` ๊ฐ€ ์žˆ์œผ๋ฉด ์„ค์ • ์ถœ์ฒ˜ opt-in ์€ ๊บผ์ง„๋‹ค(optin ์ด ํŒ์ •)."""
53
+
54
+ database_url: str | None = field(default=None, repr=False)
55
+ confirm: str | None = field(default=None, repr=False)
56
+ problems: tuple[str, ...] = ()
57
+
58
+
59
+ def config_file_path(config_path: Path | None = None) -> Path:
60
+ """์ฝ์„ ์„ค์ • ํŒŒ์ผ โ€” ๋ช…์‹œ ๊ฒฝ๋กœ > `SOLAR_EVAL_CONFIG` > ๊ธฐ๋ณธ ๊ฒฝ๋กœ(`<์„ค์ • ํ™ˆ>/solar-eval.yaml`) > ์˜› ๊ฒฝ๋กœ(์žˆ์„ ๋•Œ๋งŒ, ๊ฒฝ๊ณ ).
61
+
62
+ ๊ธฐ๋ณธ ๊ฒฝ๋กœ์— ํŒŒ์ผ์ด ์—†๊ณ  ์˜› `~/.upstage-cloud-dev/solar-eval-config.yaml` ์ด ์žˆ์œผ๋ฉด ์˜› ๊ฒƒ์„ ์ฝ๋Š”๋‹ค โ€” ์˜ฎ๊ธฐ๊ธฐ ์ „์˜ ์„ค์ •์ด
63
+ ์กฐ์šฉํžˆ ์‚ฌ๋ผ์ง€์ง€ ์•Š๊ฒŒ. ๋‘˜ ๋‹ค ์—†์œผ๋ฉด ๊ธฐ๋ณธ ๊ฒฝ๋กœ(์—†๋Š” ํŒŒ์ผ = ๋นˆ ์„ค์ •)๋‹ค.
64
+ """
65
+ if config_path:
66
+ return config_path
67
+ env_path = os.environ.get("SOLAR_EVAL_CONFIG")
68
+ if env_path is not None:
69
+ return Path(env_path)
70
+ if DEFAULT_CONFIG_PATH.exists() or not LEGACY_CONFIG_PATH.exists():
71
+ return DEFAULT_CONFIG_PATH
72
+ _warn_legacy_once(LEGACY_CONFIG_PATH, DEFAULT_CONFIG_PATH)
73
+ return LEGACY_CONFIG_PATH
74
+
75
+
76
+ @functools.cache
77
+ def _warn_legacy_once(legacy: Path, primary: Path) -> None:
78
+ """์˜› ๊ฒฝ๋กœ ๊ฒฝ๊ณ ๋Š” ๊ฒฝ๋กœ ์Œ๋งˆ๋‹ค ํ”„๋กœ์„ธ์Šค๋‹น ํ•œ ๋ฒˆ โ€” `CLIConfig` ๊ฐ€ ์—ฌ๋Ÿฌ ๋ฒˆ ๋งŒ๋“ค์–ด์ ธ๋„ ๋ฐ˜๋ณตํ•˜์ง€ ์•Š๋Š”๋‹ค."""
79
+ logger.warning(
80
+ "์˜› ์„ค์ • ๊ฒฝ๋กœ %s ๋ฅผ ์ฝ๋Š”๋‹ค โ€” %s ๋กœ ์˜ฎ๊ธด๋‹ค(make dev-docker-up ์ด ๋ณต์‚ฌํ•œ๋‹ค)", legacy, primary
81
+ )
82
+
83
+
84
+ def parse_hub(yaml_cfg: Mapping[str, Any], path: Path | None = None) -> HubConfig | None:
85
+ """`hub:` ์ ˆ โ†’ `HubConfig`(์—†๊ฑฐ๋‚˜ ๋น„์—ˆ์œผ๋ฉด None). ๋ชจ์–‘ ์˜ค๋ฅ˜๋Š” ๊ฐ’ ์—†์ด `problems` ๋กœ(ยง2.6 "์„ค์ • ๋ชจ์–‘ ์˜ค๋ฅ˜")."""
86
+ raw = yaml_cfg.get("hub")
87
+ if raw is None:
88
+ return None
89
+ if not isinstance(raw, Mapping):
90
+ return HubConfig(problems=("hub: ๋งคํ•‘์ด ์•„๋‹ˆ๋‹ค",))
91
+ problems = [f"hub.{_key_name(key)}: ๋ชจ๋ฅด๋Š” ํ‚ค" for key in raw if key not in HUB_CONFIG_KEYS]
92
+ url = _hub_url(raw.get("database_url"), problems)
93
+ confirm = _hub_confirm(raw.get("confirm"), problems)
94
+ if url is not None and path is not None:
95
+ problems += _permission_problems(path)
96
+ return HubConfig(url, confirm, tuple(problems))
97
+
98
+
99
+ def _key_name(key: Any) -> str:
100
+ return str(key)[:_KEY_NAME_MAX] if isinstance(key, str) else f"<{type(key).__name__}>"
101
+
102
+
103
+ def _hub_url(value: Any, problems: list[str]) -> str | None:
104
+ """URL ๋ฌธ์ž์—ด(์•ž๋’ค ๊ณต๋ฐฑ ์ œ๊ฑฐ). ๋ชจ์–‘์ด ํ‹€๋ ค๋„ ๋ฌธ์ž์—ด์ด๋ฉด ๋Œ๋ ค์ค€๋‹ค โ€” ๋ˆ ์„ค์ •์˜ ๊ฐ’๋„ ๊ฐ€๋ฆผยท๊ฑฐ๋ถ€ ๋Œ€์ƒ์ด๋‹ค."""
105
+ if value is None:
106
+ return None
107
+ if not isinstance(value, str):
108
+ problems.append("hub.database_url: ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋‹ค")
109
+ return None
110
+ url = value.strip()
111
+ if not url:
112
+ return None
113
+ if not url.startswith(HUB_URL_SCHEMES):
114
+ problems.append("hub.database_url: postgres:// ๋˜๋Š” postgresql:// URL ์ด ์•„๋‹ˆ๋‹ค")
115
+ return url
116
+
117
+
118
+ def _hub_confirm(value: Any, problems: list[str]) -> str | None:
119
+ if value is None:
120
+ return None
121
+ if not isinstance(value, str):
122
+ problems.append("hub.confirm: ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋‹ค")
123
+ return None
124
+ return value.strip() or None
125
+
126
+
127
+ def _permission_problems(path: Path) -> list[str]:
128
+ """์ž๊ฒฉ์ฆ๋ช… URL ์„ ๋‹ด์€ ์„ค์ • ํŒŒ์ผ์ด ๊ทธ๋ฃนยท๋‹ค๋ฅธ ์‚ฌ์šฉ์ž์—๊ฒŒ ์—ด๋ ค ์žˆ์œผ๋ฉด ๋ฌธ์ œ ํ•œ ์ค„(๊ฐ’ยท๊ฒฝ๋กœ ์—†์Œ)."""
129
+ if os.name == "nt": # POSIX ๊ถŒํ•œ ๋น„ํŠธ๊ฐ€ ๋œป์„ ๊ฐ–์ง€ ์•Š๋Š”๋‹ค
130
+ return []
131
+ try:
132
+ mode = path.stat().st_mode
133
+ except OSError as exc:
134
+ return [f"์„ค์ • ํŒŒ์ผ ๊ถŒํ•œ์„ ํ™•์ธํ•  ์ˆ˜ ์—†๋‹ค({type(exc).__name__})"]
135
+ if mode & LOOSE_MODE_BITS:
136
+ return ["์„ค์ • ํŒŒ์ผ์ด ์†Œ์œ ์ž ์ „์šฉ์ด ์•„๋‹ˆ๋‹ค โ€” chmod 600 <์„ค์ • ํŒŒ์ผ>"]
137
+ return []
138
+
139
+
140
  def default_projects_dir() -> Path:
141
  """CBS ๊ณต์šฉ ๋ฐ์ดํ„ฐ ํ™ˆ ์•„๋ž˜์˜ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ (huggingface ์บ์‹œ์™€ ๊ฐ™์€ ๋ฐฉ์‹).
142
 
143
  - ๊ธฐ๋ณธ๊ฐ’: ``~/.cache/cbs-data/solar-eval-projects``
144
  - ``CBS_DATA_HOME`` ์ด ์„ค์ •๋˜๋ฉด ๋ฐ์ดํ„ฐ ํ™ˆ ์ž์ฒด๋ฅผ ์˜ฎ๊ธด๋‹ค: ``$CBS_DATA_HOME/solar-eval-projects``
145
  - ``XDG_CACHE_HOME`` ์ด ์„ค์ •๋˜๋ฉด ์บ์‹œ ๋ฃจํŠธ๋กœ ์กด์ค‘ํ•œ๋‹ค: ``$XDG_CACHE_HOME/cbs-data/solar-eval-projects``
146
+ - ๋นˆ ``XDG_CACHE_HOME`` ์€ XDG ๊ทœ์•ฝ๋Œ€๋กœ ์—†๋Š” ๊ฒƒ์œผ๋กœ ๋ณธ๋‹ค โ€” ``poc_bundle.home.data_home`` ๊ณผ ๊ฐ™์€ ๊ทœ์น™์ด๋‹ค
147
+ (๊ณ„ํš 2026-09-27 L6 Q-L6-17 โ€” ์ „์—๋Š” ์ƒ๋Œ€ ๊ฒฝ๋กœ ``cbs-data/โ€ฆ`` ๋กœ ํ’€๋ ค ํ˜ธ์ถœ ์œ„์น˜๋งˆ๋‹ค ๋‹ฌ๋ž๋‹ค).
148
  """
149
  data_home = os.environ.get("CBS_DATA_HOME")
150
  if data_home:
151
  return Path(data_home).expanduser() / "solar-eval-projects"
152
+ cache_root = os.environ.get("XDG_CACHE_HOME") or "~/.cache"
153
  return Path(cache_root).expanduser() / "cbs-data" / "solar-eval-projects"
154
 
155
 
 
193
 
194
  def _load_yaml_config(config_path: Path | None = None) -> dict[str, Any]:
195
  """YAML ์„ค์ • ํŒŒ์ผ ๋กœ๋“œ. ํŒŒ์ผ ์—†์œผ๋ฉด ๋นˆ dict ๋ฐ˜ํ™˜."""
196
+ path = config_file_path(config_path)
197
  if not path.is_file():
198
  return {}
199
  try:
 
201
  data = yaml.safe_load(f)
202
  return data if isinstance(data, dict) else {}
203
  except Exception as e:
204
+ # ์˜ˆ์™ธ ์›๋ฌธ์€ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค โ€” YAML ์˜ค๋ฅ˜ ๋ฌธ๊ตฌ๊ฐ€ ์ค„ ์กฐ๊ฐ(์ž๊ฒฉ์ฆ๋ช… URL ํฌํ•จ)์„ ๋‹ด์„ ์ˆ˜ ์žˆ๋‹ค(L10 ยง2.6 "๋น„๋ฐ€").
205
+ logger.warning("Failed to load config from %s: %s", path, type(e).__name__)
206
  return {}
207
 
208
 
 
211
 
212
  Priority (low โ†’ high):
213
  1. Hard-coded defaults (projects_dir: ~/.cache/cbs-data/solar-eval-projects)
214
+ 2. YAML config file (~/.config/cbs-arsenal/solar-eval.yaml, ์˜› ~/.upstage-cloud-dev/solar-eval-config.yaml) โ€” `hub:` ์ ˆ ํฌํ•จ(๋ชจ๋“ˆ ์„ค๋ช…)
215
+ 3. Environment variables (SOLAR_EVAL_URL, CBS_DATA_HOME, SOLAR_EVAL_PROJECTS, HUB_DATABASE_URL, etc.)
216
  4. CLI options (--remote, etc.)
217
  """
218
 
219
  def __init__(self, config_path: Path | None = None) -> None:
220
+ path = config_file_path(config_path)
221
+ yaml_cfg = _load_yaml_config(path)
222
+ #: run ์ข…๋ฃŒ ํ›… hub ๋‹ค๋ฆฌ opt-in(์„ค์ • ์ถœ์ฒ˜) โ€” ํ•ด์„์€ `hub_hook.optin.resolve_hub_optin`.
223
+ self.hub: HubConfig | None = parse_hub(yaml_cfg, path)
224
 
225
  yaml_remote = yaml_cfg.get("remote_url") or yaml_cfg.get("remote")
226
  self.remote_url: str | None = os.environ.get("SOLAR_EVAL_URL", yaml_remote)
 
257
  if openai_cfg.get("api_key") and not os.environ.get("OPENAI_API_KEY"):
258
  os.environ["OPENAI_API_KEY"] = openai_cfg["api_key"]
259
 
260
+ timely_cfg = yaml_cfg.get("timely", {})
261
+ if timely_cfg.get("api_key") and not os.environ.get("TIMELY_API_KEY"):
262
+ os.environ["TIMELY_API_KEY"] = timely_cfg["api_key"]
263
+
264
  @property
265
  def is_remote(self) -> bool:
266
  return self.remote_url is not None
solar_eval/cli/formatters.py CHANGED
@@ -29,36 +29,69 @@ def format_overall_score(score: float | None) -> str:
29
 
30
 
31
  def format_projects_table(projects: list[dict[str, Any]]) -> str:
32
- rows = [[
33
- p.get("name", ""),
34
- p.get("display_name", ""),
35
- p.get("description", "")[:50],
36
- p.get("task_count", 0),
37
- ] for p in projects]
38
- return tabulate(rows, headers=["Name", "Display Name", "Description", "Tasks"], tablefmt="simple")
 
 
 
 
 
39
 
40
 
41
  def format_prompts_table(prompts: list[dict[str, Any]]) -> str:
42
- rows = [[
43
- p.get("id", "")[:12],
44
- p.get("task", ""),
45
- f"v{p.get('version', '?')}",
46
- p.get("model", ""),
47
- p.get("temperature", ""),
48
- (p.get("system_prompt", "") or "")[:40] + "...",
49
- ] for p in prompts]
50
- return tabulate(rows, headers=["ID", "Task", "Version", "Model", "Temp", "Prompt Preview"], tablefmt="simple")
 
 
 
 
 
 
 
51
 
52
 
53
  def format_runs_table(runs: list[dict[str, Any]]) -> str:
54
- rows = [[
55
- r.get("id", "")[:12],
56
- r.get("task", ""),
57
- format_status(r.get("status", "")),
58
- f"{r.get('completed_samples', 0)}/{r.get('total_samples', 0)}",
59
- r.get("created_at", "")[:19],
60
- ] for r in runs]
61
- return tabulate(rows, headers=["ID", "Task", "Status", "Progress", "Created"], tablefmt="simple")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
 
64
  def format_results_table(results: list[dict[str, Any]]) -> str:
@@ -66,15 +99,19 @@ def format_results_table(results: list[dict[str, Any]]) -> str:
66
  for r in results:
67
  inp = str(r.get("input", ""))[:40]
68
  out = str(r.get("output", ""))[:40]
69
- rows.append([
70
- r.get("sample_idx", ""),
71
- inp + ("..." if len(inp) >= 40 else ""),
72
- out + ("..." if len(out) >= 40 else ""),
73
- r.get("input_tokens", ""),
74
- r.get("output_tokens", ""),
75
- f"{r.get('inference_time_ms', 0):.0f}ms",
76
- ])
77
- return tabulate(rows, headers=["#", "Input", "Output", "In Tok", "Out Tok", "Time"], tablefmt="simple")
 
 
 
 
78
 
79
 
80
  def format_evaluation_detail(evaluation: dict[str, Any]) -> str:
@@ -91,14 +128,21 @@ def format_evaluation_detail(evaluation: dict[str, Any]) -> str:
91
 
92
 
93
  def format_dashboard_table(dashboard: dict[str, Any]) -> str:
94
- rows = [[
95
- r.get("run_id", "")[:12],
96
- r.get("task", ""),
97
- f"v{r.get('prompt_version', '?')}",
98
- r.get("model", ""),
99
- format_overall_score(r.get("overall_score")),
100
- format_scores(r.get("scores")),
101
- r.get("total_samples", ""),
102
- r.get("date", "")[:10],
103
- ] for r in dashboard.get("runs", [])]
104
- return tabulate(rows, headers=["Run ID", "Task", "Version", "Model", "Score", "Metrics", "Samples", "Date"], tablefmt="simple")
 
 
 
 
 
 
 
 
29
 
30
 
31
  def format_projects_table(projects: list[dict[str, Any]]) -> str:
32
+ rows = [
33
+ [
34
+ p.get("name", ""),
35
+ p.get("display_name", ""),
36
+ p.get("description", "")[:50],
37
+ p.get("task_count", 0),
38
+ ]
39
+ for p in projects
40
+ ]
41
+ return tabulate(
42
+ rows, headers=["Name", "Display Name", "Description", "Tasks"], tablefmt="simple"
43
+ )
44
 
45
 
46
  def format_prompts_table(prompts: list[dict[str, Any]]) -> str:
47
+ rows = [
48
+ [
49
+ p.get("id", "")[:12],
50
+ p.get("task", ""),
51
+ f"v{p.get('version', '?')}",
52
+ p.get("model", ""),
53
+ p.get("temperature", ""),
54
+ (p.get("system_prompt", "") or "")[:40] + "...",
55
+ ]
56
+ for p in prompts
57
+ ]
58
+ return tabulate(
59
+ rows,
60
+ headers=["ID", "Task", "Version", "Model", "Temp", "Prompt Preview"],
61
+ tablefmt="simple",
62
+ )
63
 
64
 
65
  def format_runs_table(runs: list[dict[str, Any]]) -> str:
66
+ rows = [
67
+ [
68
+ r.get("id", "")[:12],
69
+ r.get("task", ""),
70
+ format_status(r.get("status", "")),
71
+ f"{r.get('completed_samples', 0)}/{r.get('total_samples', 0)}",
72
+ r.get("created_at", "")[:19],
73
+ ]
74
+ for r in runs
75
+ ]
76
+ return tabulate(
77
+ rows, headers=["ID", "Task", "Status", "Progress", "Created"], tablefmt="simple"
78
+ )
79
+
80
+
81
+ # ์‚ฌ์šฉ๋Ÿ‰ยท์ง€์—ฐ์ด ์ธก์ •๋˜์ง€ ์•Š์€ ์นธ(`None`) -- 0 ๊ณผ ๊ตฌ๋ถ„๋˜๊ฒŒ ์ฐ๋Š”๋‹ค(L6b B1 ยท D-M2-24).
82
+ MISSING_CELL = "โ€”"
83
+
84
+
85
+ def _usage_cell(value: Any) -> Any:
86
+ """ํ† ํฐ ์นธ -- `None` ์ด๋ฉด `โ€”`, ๊ทธ ๋ฐ–์€ ๊ทธ๋Œ€๋กœ(ํ‚ค๊ฐ€ ์•„์˜ˆ ์—†์„ ๋•Œ์˜ ๋นˆ ์นธ ํฌํ•จ)."""
87
+ return MISSING_CELL if value is None else value
88
+
89
+
90
+ def format_latency(value: Any) -> str:
91
+ """์ง€์—ฐ ์นธ -- ์ˆซ์ž์ผ ๋•Œ๋งŒ `โ€ฆms`, ๊ทธ ๋ฐ–(`None`ยทํ‚ค ์—†์Œยท๋ฌธ์ž์—ดยท๋ถˆ๋ฆฌ์–ธ)์€ `โ€”`."""
92
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
93
+ return MISSING_CELL
94
+ return f"{value:.0f}ms"
95
 
96
 
97
  def format_results_table(results: list[dict[str, Any]]) -> str:
 
99
  for r in results:
100
  inp = str(r.get("input", ""))[:40]
101
  out = str(r.get("output", ""))[:40]
102
+ rows.append(
103
+ [
104
+ r.get("sample_idx", ""),
105
+ inp + ("..." if len(inp) >= 40 else ""),
106
+ out + ("..." if len(out) >= 40 else ""),
107
+ _usage_cell(r.get("input_tokens", "")),
108
+ _usage_cell(r.get("output_tokens", "")),
109
+ format_latency(r.get("inference_time_ms")),
110
+ ]
111
+ )
112
+ return tabulate(
113
+ rows, headers=["#", "Input", "Output", "In Tok", "Out Tok", "Time"], tablefmt="simple"
114
+ )
115
 
116
 
117
  def format_evaluation_detail(evaluation: dict[str, Any]) -> str:
 
128
 
129
 
130
  def format_dashboard_table(dashboard: dict[str, Any]) -> str:
131
+ rows = [
132
+ [
133
+ r.get("run_id", "")[:12],
134
+ r.get("task", ""),
135
+ f"v{r.get('prompt_version', '?')}",
136
+ r.get("model", ""),
137
+ format_overall_score(r.get("overall_score")),
138
+ format_scores(r.get("scores")),
139
+ r.get("total_samples", ""),
140
+ r.get("date", "")[:10],
141
+ ]
142
+ for r in dashboard.get("runs", [])
143
+ ]
144
+ return tabulate(
145
+ rows,
146
+ headers=["Run ID", "Task", "Version", "Model", "Score", "Metrics", "Samples", "Date"],
147
+ tablefmt="simple",
148
+ )
solar_eval/cli/main.py CHANGED
@@ -59,6 +59,7 @@ from solar_eval.cli.commands.runs import runs_group
59
  from solar_eval.cli.commands.eval import eval_group
60
  from solar_eval.cli.commands.dashboard import dashboard_group
61
  from solar_eval.cli.commands.datasets import datasets_group
 
62
 
63
  from solar_eval.cli.commands.compare import compare_command
64
  from solar_eval.cli.commands.quickrun import quick_run
@@ -73,6 +74,8 @@ solar_eval_cli.add_command(datasets_group)
73
  solar_eval_cli.add_command(compare_command)
74
  solar_eval_cli.add_command(quick_run)
75
  solar_eval_cli.add_command(sync_to_mongodb)
 
 
76
 
77
 
78
  def entrypoint() -> None:
 
59
  from solar_eval.cli.commands.eval import eval_group
60
  from solar_eval.cli.commands.dashboard import dashboard_group
61
  from solar_eval.cli.commands.datasets import datasets_group
62
+ from solar_eval.cli.commands.score import score_group
63
 
64
  from solar_eval.cli.commands.compare import compare_command
65
  from solar_eval.cli.commands.quickrun import quick_run
 
74
  solar_eval_cli.add_command(compare_command)
75
  solar_eval_cli.add_command(quick_run)
76
  solar_eval_cli.add_command(sync_to_mongodb)
77
+ # ์˜คํ”„๋ผ์ธ ์žฌ์ฑ„์ (`score proofread-diff`) โ€” ๊ณ„ํš 2026-09-26 L3b ยง2.1.
78
+ solar_eval_cli.add_command(score_group)
79
 
80
 
81
  def entrypoint() -> None:
solar_eval/config.py CHANGED
@@ -7,6 +7,9 @@ class ProviderConfig(BaseSettings):
7
  upstage_api_key: str = ""
8
  upstage_api_base: str = "https://api.upstage.ai/v1"
9
  openai_api_key: str = ""
 
 
 
10
  hf_token: str = ""
11
  default_model: str = "solar-pro2"
12
 
 
7
  upstage_api_key: str = ""
8
  upstage_api_base: str = "https://api.upstage.ai/v1"
9
  openai_api_key: str = ""
10
+ # Timely Router (OpenAI ํ˜ธํ™˜ ๋ฉ€ํ‹ฐ๋ชจ๋ธ ๋ผ์šฐํ„ฐ) โ€” ์Šคํ… `model: timely:<๋ชจ๋ธ>` ์ด ์“ด๋‹ค
11
+ timely_api_key: str = ""
12
+ timely_api_base: str = "https://api.timelyrouter.ai/v1"
13
  hf_token: str = ""
14
  default_model: str = "solar-pro2"
15
 
solar_eval/core/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (713 Bytes). View file
 
solar_eval/core/__pycache__/dataset_loader.cpython-312.pyc ADDED
Binary file (7.55 kB). View file
 
solar_eval/core/__pycache__/pipeline_compose.cpython-312.pyc ADDED
Binary file (11 kB). View file
 
solar_eval/core/dataset_loader.py CHANGED
@@ -1,6 +1,9 @@
1
  import csv
 
 
2
  import json
3
  import logging
 
4
  from pathlib import Path
5
 
6
  from huggingface_hub import hf_hub_download, list_repo_files
@@ -10,6 +13,34 @@ from solar_eval.config import get_provider_config
10
  logger = logging.getLogger(__name__)
11
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  class DatasetLoader:
14
  """Loads datasets from HuggingFace repos or local filesystem."""
15
 
@@ -37,24 +68,34 @@ class DatasetLoader:
37
  return self.base_dir / repo / path
38
  return repo_path / path
39
 
40
- def load_jsonl(
41
  self, repo: str, path: str, source: str = "huggingface", force: bool = False
42
- ) -> list[dict]:
43
- """Load and parse a JSONL file from HuggingFace or local filesystem."""
 
 
 
 
44
  if source == "local":
45
  local_path = self._resolve_local_path(repo, path)
46
  if not local_path.exists():
47
  raise FileNotFoundError(f"Local dataset not found: {local_path}")
48
  else:
49
  local_path = self.download_file(repo, path, force=force)
50
- records = []
51
- with open(local_path, encoding="utf-8") as f:
52
- for line in f:
53
- line = line.strip()
54
- if line:
55
- records.append(json.loads(line))
56
  logger.info(f"Loaded {len(records)} records from {local_path}")
57
- return records
 
 
 
 
 
 
 
 
 
 
58
 
59
  def load_csv(
60
  self, repo: str, path: str, source: str = "huggingface", force: bool = False
 
1
  import csv
2
+ import hashlib
3
+ import io
4
  import json
5
  import logging
6
+ from dataclasses import dataclass
7
  from pathlib import Path
8
 
9
  from huggingface_hub import hf_hub_download, list_repo_files
 
13
  logger = logging.getLogger(__name__)
14
 
15
 
16
+ @dataclass(frozen=True)
17
+ class LoadedJsonl:
18
+ """ํ•œ ๋ฒˆ ์ฝ์€ JSONL -- ํŒŒ์‹ฑํ•œ ๋ ˆ์ฝ”๋“œ์™€ **๊ฐ™์€ ๋ฐ”์ดํŠธ**์˜ sha256 (๊ณ„ํš D16).
19
+
20
+ `sha256` ์€ 64์ž ์†Œ๋ฌธ์ž hex ๋‹ค(`sha256:` ์ ‘๋‘์–ด ์—†์Œ). ํŒŒ์ผ sha ์™€ ํŒŒ์‹ฑ์ด ์„œ๋กœ
21
+ ๋‹ค๋ฅธ read ์—์„œ ๋‚˜์˜ค๋ฉด ๊ทธ ์‚ฌ์ด์— ํŒŒ์ผ์ด ๋ฐ”๋€Œ์—ˆ์„ ๋•Œ run.json ์ด ๊ฑฐ์ง“์„ ์ ๋Š”๋‹ค.
22
+ """
23
+
24
+ records: list[dict]
25
+ sha256: str
26
+ local_path: Path
27
+
28
+
29
+ def parse_jsonl_bytes(data: bytes) -> list[dict]:
30
+ """JSONL ๋ฐ”์ดํŠธ๋ฅผ ๋ ˆ์ฝ”๋“œ ๋ชฉ๋ก์œผ๋กœ. ์˜ค๋Š˜ `open()` ๋ฐ˜๋ณต๊ณผ **๊ฐ™์€ ์ค„ ๊ทœ์น™**์ด๋‹ค.
31
+
32
+ universal newlines(`\n`ยท`\r\n`ยท๋‹จ๋… `\r` ๋ชจ๋‘ ์ค„ ๋)๋กœ ์ค„์„ ๋‚˜๋ˆ„๊ณ , `strip()` ๋’ค
33
+ ๋นˆ ์ค„์€ ๊ฑด๋„ˆ๋›ด๋‹ค -- `sample_idx`(๋ ˆ์ฝ”๋“œ ์œ„์น˜)๊ฐ€ ์˜› run ๊ณผ ์–ด๊ธ‹๋‚˜์ง€ ์•Š์•„์•ผ ํ•œ๋‹ค.
34
+ """
35
+ records = []
36
+ with io.TextIOWrapper(io.BytesIO(data), encoding="utf-8") as f:
37
+ for line in f:
38
+ line = line.strip()
39
+ if line:
40
+ records.append(json.loads(line))
41
+ return records
42
+
43
+
44
  class DatasetLoader:
45
  """Loads datasets from HuggingFace repos or local filesystem."""
46
 
 
68
  return self.base_dir / repo / path
69
  return repo_path / path
70
 
71
+ def read_jsonl(
72
  self, repo: str, path: str, source: str = "huggingface", force: bool = False
73
+ ) -> LoadedJsonl:
74
+ """JSONL ์„ **๋ฐ”์ดํŠธ๋กœ ํ•œ ๋ฒˆ** ์ฝ์–ด sha256 ๊ณผ ๋ ˆ์ฝ”๋“œ๋ฅผ ํ•จ๊ป˜ ๋Œ๋ ค์ค€๋‹ค (D16).
75
+
76
+ sha ๋Š” `hashlib` ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค -- ์ด ๋ชจ๋“ˆ์€ ๋ฒค๋” ์‚ฌ๋ณธ(HF Space)์— ์‹ค๋ฆฌ๋ฏ€๋กœ
77
+ poc-bundle ํŒจํ‚ค์ง€๋ฅผ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š”๋‹ค(D18).
78
+ """
79
  if source == "local":
80
  local_path = self._resolve_local_path(repo, path)
81
  if not local_path.exists():
82
  raise FileNotFoundError(f"Local dataset not found: {local_path}")
83
  else:
84
  local_path = self.download_file(repo, path, force=force)
85
+ data = Path(local_path).read_bytes()
86
+ records = parse_jsonl_bytes(data)
 
 
 
 
87
  logger.info(f"Loaded {len(records)} records from {local_path}")
88
+ return LoadedJsonl(
89
+ records=records,
90
+ sha256=hashlib.sha256(data).hexdigest(),
91
+ local_path=Path(local_path),
92
+ )
93
+
94
+ def load_jsonl(
95
+ self, repo: str, path: str, source: str = "huggingface", force: bool = False
96
+ ) -> list[dict]:
97
+ """Load and parse a JSONL file from HuggingFace or local filesystem."""
98
+ return self.read_jsonl(repo, path, source=source, force=force).records
99
 
100
  def load_csv(
101
  self, repo: str, path: str, source: str = "huggingface", force: bool = False
solar_eval/core/dataset_prep.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """run ์‹œ์ž‘ ์ „ ๋ฐ์ดํ„ฐ์…‹ ์ค€๋น„ -- ํ•œ ๋ฒˆ ์ฝ๊ณ , ๊ณ ๋ฅด๊ณ , ํ–‰ ๋ฉ”ํƒ€ ๊ทœ์น™์„ ๊ฒ€์ฆํ•œ๋‹ค (๊ณ„ํš D3ยทD13ยทD16).
2
+
3
+ CLI(`runs start/batch`)๋Š” run ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๋งŒ๋“ค๊ธฐ **์ „์—** `prepare_dataset` ์„ ๋ถˆ๋Ÿฌ
4
+ 0ํ–‰ ์„ ํƒยท๋ผ๋ฒจ ๋ˆ„๋ฝยท`meta_fields` ์˜ค๋ฅ˜๋ฅผ ๋จผ์ € ํ„ฐ๋œจ๋ฆฌ๊ณ , ๊ฐ™์€ `PreparedDataset` ์„
5
+ ๋Ÿฌ๋„ˆ(`execute_run(prepared=...)`)์™€ run.json ๊ธฐ๋ก(sha256ยทํ–‰ ์ˆ˜ยท์„ ํƒ)์— ํ•จ๊ป˜ ๋„˜๊ธด๋‹ค.
6
+ ๊ทธ๋ž˜์„œ run.json ์˜ sha256 ๊ณผ ์‹ค์ œ๋กœ ๋ˆ ํ–‰์ด **๊ฐ™์€ ๋ฐ”์ดํŠธ**์—์„œ ๋‚˜์˜จ๋‹ค.
7
+
8
+ ๋Ÿฌ๋„ˆ๋Š” `prepared` ๊ฐ€ ์—†์œผ๋ฉด(์„œ๋น„์Šคยท์˜› ํ˜ธ์ถœ์ž) ๊ฐ™์€ ํ•จ์ˆ˜๋กœ ์Šค์Šค๋กœ ์ค€๋น„ํ•œ๋‹ค.
9
+
10
+ ์ด ๋ชจ๋“ˆ์€ poc-bundle ํŒจํ‚ค์ง€์™€ packaging ์„ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š”๋‹ค(D18).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import copy
16
+ from dataclasses import dataclass
17
+ from typing import Any, Mapping
18
+
19
+ from solar_eval.core.dataset_loader import DatasetLoader, LoadedJsonl
20
+ from solar_eval.core.row_filter import (
21
+ RowSelection,
22
+ SelectionError,
23
+ Where,
24
+ combine,
25
+ format_indices,
26
+ parse_where,
27
+ select,
28
+ )
29
+
30
+
31
+ class DatasetPrepError(ValueError):
32
+ """ํƒœ์Šคํฌ์˜ ํ–‰ ๋ฉ”ํƒ€ยทwhere ์„ ์–ธ์ด ๊ทœ์น™์— ๋งž์ง€ ์•Š์„ ๋•Œ -- run ์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค."""
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class PreparedDataset:
37
+ """run ์ด ๋Œ ํ–‰ ์ง‘ํ•ฉ. `selected` ๋Š” `(์›๋ณธ ํŒŒ์ผ์˜ 0-based ์œ„์น˜, ํ–‰)` ํŠœํ”Œ๋“ค์ด๋‹ค."""
38
+
39
+ loaded: LoadedJsonl
40
+ selected: tuple[tuple[int, dict[str, Any]], ...]
41
+ rows_total: int
42
+ meta_fields: tuple[str, ...] | None
43
+ selection: RowSelection | None = None
44
+
45
+ @property
46
+ def rows_selected(self) -> int:
47
+ return len(self.selected)
48
+
49
+ @property
50
+ def sha256(self) -> str:
51
+ return self.loaded.sha256
52
+
53
+ @property
54
+ def selected_indices(self) -> frozenset[int]:
55
+ return frozenset(idx for idx, _ in self.selected)
56
+
57
+
58
+ def reject_defaults_where(project_config: Mapping[str, Any]) -> None:
59
+ """`defaults.where` ๋ฅผ ๊ฑฐ๋ถ€ํ•œ๋‹ค (D1).
60
+
61
+ `_resolve_defaults` ๋Š” ํ‚ค ๋‹จ์œ„ ์–•์€ ๋ณ‘ํ•ฉ์ด๋ผ `defaults.where` ๊ฐ€ ๋ผ๋ฒจ ์—†๋Š” ํƒœ์Šคํฌ์—๋„
62
+ ํผ์ ธ ๊ทธ run ์ด ์ „๋ถ€ ์ฃฝ๊ณ , ํƒœ์Šคํฌ์— where ๊ฐ€ ์žˆ์œผ๋ฉด ํ†ต์งธ๋กœ ๋ฌด์‹œ๋œ๋‹ค("๋ณ‘ํ•ฉ"์ด ์•„๋‹ˆ๋ผ
63
+ "๋Œ€์ฒด"). ๋กœ๋“œ๋œ dict ์— `defaults` ํ‚ค๊ฐ€ ๋ณ‘ํ•ฉ ๋’ค์—๋„ ๋‚จ์•„ ์žˆ์œผ๋ฏ€๋กœ ๊ทธ๊ฒƒ์„ ๋ณธ๋‹ค.
64
+ ๋กœ๋“œ ์‹œ์ ์ด ์•„๋‹ˆ๋ผ run ์‹œ์ž‘ ์‹œ ๊ฒ€์‚ฌํ•œ๋‹ค -- ํ•œ ํ”„๋กœ์ ํŠธ์˜ ์ž˜๋ชป๋œ ์„ ์–ธ์ด ๋‹ค๋ฅธ
65
+ ํ”„๋กœ์ ํŠธ ๋กœ๋“œ๋ฅผ ๋ง‰์ง€ ์•Š๊ฒŒ.
66
+ """
67
+ defaults = project_config.get("defaults") or {}
68
+ if isinstance(defaults, Mapping) and "where" in defaults:
69
+ name = project_config.get("name", "?")
70
+ raise DatasetPrepError(
71
+ f"project {name!r}: defaults.where is not allowed -- "
72
+ "declare where on a task or pass --where on the CLI"
73
+ )
74
+
75
+
76
+ def _data_keys(task_config: Mapping[str, Any]) -> set[str]:
77
+ """์ž…๋ ฅยท์ •๋‹ต์œผ๋กœ ์ด๋ฏธ ์“ฐ๋Š” ๋ฐ์ดํ„ฐ ํ–‰ ํ‚ค -- meta ๋กœ ์ค‘๋ณต ๋ณต์ œํ•˜์ง€ ์•Š๋Š”๋‹ค."""
78
+ keys = set(task_config.get("input_fields") or [])
79
+ golden_field = task_config.get("golden_field")
80
+ if golden_field:
81
+ keys.add(golden_field)
82
+ golden_fields = task_config.get("golden_fields") or {}
83
+ if isinstance(golden_fields, Mapping):
84
+ keys.update(v for v in golden_fields.values() if isinstance(v, str))
85
+ return keys
86
+
87
+
88
+ def validate_meta_fields(task_config: Mapping[str, Any]) -> tuple[str, ...] | None:
89
+ """ํƒœ์Šคํฌ `meta_fields` ๊ฒ€์ฆ (D13) -- ๋น„์–ด ์žˆ์ง€ ์•Š์€ ๋ฌธ์ž์—ด ๋ฆฌ์ŠคํŠธ, ์ค‘๋ณตยท์ž…๋ ฅ/์ •๋‹ต ๊ฒน์นจ ๋ถˆ๊ฐ€.
90
+
91
+ ์„ ์–ธ์ด ์—†์œผ๋ฉด `None`(results ํ–‰์— `meta` ํ‚ค๋ฅผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค -- ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์˜ ๋ฐ”์ดํŠธ ๋ถˆ๋ณ€).
92
+ """
93
+ raw = task_config.get("meta_fields")
94
+ if raw is None:
95
+ return None
96
+ task = task_config.get("name", "?")
97
+ if not isinstance(raw, list) or not raw:
98
+ raise DatasetPrepError(f"task {task!r}: meta_fields must be a non-empty list of strings")
99
+ if not all(isinstance(f, str) and f.strip() for f in raw):
100
+ raise DatasetPrepError(f"task {task!r}: meta_fields values must be non-empty strings")
101
+ duplicates = sorted({f for f in raw if raw.count(f) > 1})
102
+ if duplicates:
103
+ raise DatasetPrepError(f"task {task!r}: meta_fields has duplicates: {duplicates}")
104
+ overlap = sorted(set(raw) & _data_keys(task_config))
105
+ if overlap:
106
+ raise DatasetPrepError(f"task {task!r}: meta_fields overlap input/golden fields: {overlap}")
107
+ return tuple(raw)
108
+
109
+
110
+ def row_meta(row: Mapping[str, Any], meta_fields: tuple[str, ...] | None) -> dict[str, Any] | None:
111
+ """๋ฐ์ดํ„ฐ ํ–‰์—์„œ `meta_fields` ํ‚ค๋งŒ ๊ณจ๋ผ ๋ณต์‚ฌํ•œ๋‹ค. ํ–‰์— ์—†๋Š” ํ‚ค๋Š” **์ƒ๋žต**ํ•œ๋‹ค.
112
+
113
+ ๊ฐ’์€ ๊นŠ์€ ๋ณต์‚ฌ๋ณธ์ด๋‹ค -- ๊ฒฐ๊ณผ ํ–‰์ด ๋ฐ์ดํ„ฐ์…‹ ํ–‰ ๊ฐ์ฒด๋ฅผ ๊ณต์œ ํ•˜์ง€ ์•Š๊ฒŒ.
114
+ """
115
+ if meta_fields is None:
116
+ return None
117
+ return {key: copy.deepcopy(row[key]) for key in meta_fields if key in row}
118
+
119
+
120
+ def build_selection(
121
+ task_config: Mapping[str, Any], cli_where: Where | None = None, limit: int | None = None
122
+ ) -> RowSelection:
123
+ """ํƒœ์Šคํฌ `where` ์™€ CLI where(`Where` ๋˜๋Š” `None`)๋ฅผ ํ•ฉ์ณ run ์˜ ์„ ํƒ์„ ๋งŒ๋“ ๋‹ค.
124
+
125
+ ์„ ํƒ์€ CLI ๊ฐ€ **๋ช…์‹œ์ ์œผ๋กœ** ๋งŒ๋“ค์–ด ๋Ÿฌ๋„ˆ์— ๋„˜๊ธด๋‹ค -- ๋Ÿฌ๋„ˆ๋Š” `task_config["where"]` ๋ฅผ
126
+ ์Šค์Šค๋กœ ์ฝ์ง€ ์•Š๋Š”๋‹ค(D3).
127
+ """
128
+ task_name = task_config.get("name", "?")
129
+ task_where = parse_where(task_config.get("where"), origin=f"task {task_name!r}")
130
+ return RowSelection(where=combine(task_where, cli_where), limit=limit or None)
131
+
132
+
133
+ def prepare_dataset(
134
+ loader: DatasetLoader,
135
+ project_config: Mapping[str, Any],
136
+ task_config: Mapping[str, Any],
137
+ selection: RowSelection | None,
138
+ ) -> PreparedDataset:
139
+ """๋ฐ์ดํ„ฐ์…‹์„ ํ•œ ๋ฒˆ ์ฝ๊ณ  ์„ ํƒ์„ ์ ์šฉํ•œ๋‹ค. 0ํ–‰ยท๋ผ๋ฒจ ๋ˆ„๋ฝยทmeta ์˜ค๋ฅ˜๋Š” ์—ฌ๊ธฐ์„œ ํ„ฐ์ง„๋‹ค."""
140
+ meta_fields = validate_meta_fields(task_config)
141
+ dataset_config = project_config.get("dataset") or {}
142
+ loaded = loader.read_jsonl(
143
+ dataset_config.get("repo", ""),
144
+ task_config.get("dataset_path", ""),
145
+ source=dataset_config.get("source", "huggingface"),
146
+ )
147
+ selected = select(loaded.records, selection)
148
+ return PreparedDataset(
149
+ loaded=loaded,
150
+ selected=tuple(selected),
151
+ rows_total=len(loaded.records),
152
+ meta_fields=meta_fields,
153
+ selection=selection,
154
+ )
155
+
156
+
157
+ def check_resume_indices(prepared: PreparedDataset, completed_indices: set[int]) -> None:
158
+ """์ด๋ฏธ ๋๋‚œ `sample_idx` ๊ฐ€ ์„ ํƒ ๋ฐ–์ด๋ฉด ์‹คํŒจํ•œ๋‹ค (D3 resume ๊ฐ€๋“œ).
159
+
160
+ ์„ ํƒ ๋ฐ– ๊ฒฐ๊ณผ๊ฐ€ ์„ž์ธ results ๋Š” ์ด run ์˜ ์„ ํƒ์œผ๋กœ ์„ค๋ช…๋˜์ง€ ์•Š๋Š”๋‹ค -- ์„ ํƒ์ด๋‚˜
161
+ ๋ฐ์ดํ„ฐ์…‹์ด run ์‹œ์ž‘ ๋’ค ๋ฐ”๋€Œ์—ˆ๋‹ค๋Š” ๋œป์ด๋‹ค.
162
+ """
163
+ outside = sorted(completed_indices - prepared.selected_indices)
164
+ if outside:
165
+ raise SelectionError(
166
+ "completed results are outside this run's selection "
167
+ f"({format_indices(outside)}) "
168
+ "-- the selection or dataset changed since the run started; start a new run"
169
+ )
solar_eval/core/fingerprint.py CHANGED
@@ -3,7 +3,7 @@
3
  ์™œ ํ•„์š”ํ•œ๊ฐ€: ์ด ์—”์ง„์˜ ์ž์‚ฐ์€ ์˜ค๋ž˜ ์ด๋ฆ„์œผ๋กœ๋งŒ ์‹๋ณ„๋ผ ์™”๊ณ (`prompt_dev_v23`
4
  โ†” `pipeline_dev_v24` ์ฒ˜๋Ÿผ ์ง์˜ ๋ฒˆํ˜ธ๊ฐ€ ์–ด๊ธ‹๋‚˜๋Š” ์ผ์ด ์žฆ์•˜๋‹ค), ๊ทธ๋ž˜์„œ
5
  "์ด๋ฆ„์ด ๋‹ค๋ฅธ ๋‘ run ์ด ์‚ฌ์‹ค ๊ฐ™์€ ์„ค์ •" ์ธ์ง€๋ฅผ ์‚ฌ๋žŒ์ด ํ•ด์‹œ๋ฅผ ๋– ์„œ ๋Œ€์กฐํ•ด์•ผ ํ–ˆ๋‹ค
6
- (`insights/lab_notes_archive.md#260803_etd_regression_prod_vs_v24` ์˜ '์ด๋ฆ„ ๋งคํ•‘' ์ ˆ์ด ๊ทธ ํ”์ ์ด๋‹ค).
7
  run ์— ์ง€๋ฌธ์„ ์ฐ์–ด ๋‘๋ฉด ๊ทธ ๋Œ€์กฐ๊ฐ€ ์กฐํšŒ ํ•œ ๋ฒˆ์œผ๋กœ ๋๋‚œ๋‹ค.
8
 
9
  ์ง€๋ฌธ์€ **ํ•ด์„์ด ๋๋‚œ ์„ค์ •**์„ ๋Œ€์ƒ์œผ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค. `extends`/override ๋กœ ์กฐ๋ฆฝํ•œ
@@ -16,7 +16,7 @@ run ์— ์ง€๋ฌธ์„ ์ฐ์–ด ๋‘๋ฉด ๊ทธ ๋Œ€์กฐ๊ฐ€ ์กฐํšŒ ํ•œ ๋ฒˆ์œผ๋กœ ๋๋‚œ๋‹ค.
16
 
17
  import hashlib
18
  import json
19
- from collections.abc import Mapping, Sequence
20
  from pathlib import Path
21
  from typing import Any
22
 
@@ -84,16 +84,28 @@ def prompt_fingerprint(path: Path) -> str | None:
84
  return None
85
 
86
 
87
- def _collect_prompt_assets(root: Path) -> dict[str, str]:
88
- """ํ”„๋กฌํ”„ํŠธ ๋””๋ ‰ํ† ๋ฆฌ์˜ `{์ƒ๋Œ€๊ฒฝ๋กœ: ํŒŒ์ผ์ง€๋ฌธ}` ๋งต. ์ˆจ๊น€ ํŒŒ์ผ์€ ์ œ์™ธํ•œ๋‹ค."""
89
- assets: dict[str, str] = {}
 
 
 
 
 
90
  for file in sorted(root.rglob("*")):
91
  if not file.is_file() or file.suffix.lower() not in PROMPT_ASSET_SUFFIXES:
92
  continue
93
  if any(part.startswith(".") for part in file.relative_to(root).parts):
94
  continue
95
- assets[file.relative_to(root).as_posix()] = _file_digest(file)
96
- return assets
 
 
 
 
 
 
 
97
 
98
 
99
  def _file_digest(path: Path) -> str:
 
3
  ์™œ ํ•„์š”ํ•œ๊ฐ€: ์ด ์—”์ง„์˜ ์ž์‚ฐ์€ ์˜ค๋ž˜ ์ด๋ฆ„์œผ๋กœ๋งŒ ์‹๋ณ„๋ผ ์™”๊ณ (`prompt_dev_v23`
4
  โ†” `pipeline_dev_v24` ์ฒ˜๋Ÿผ ์ง์˜ ๋ฒˆํ˜ธ๊ฐ€ ์–ด๊ธ‹๋‚˜๋Š” ์ผ์ด ์žฆ์•˜๋‹ค), ๊ทธ๋ž˜์„œ
5
  "์ด๋ฆ„์ด ๋‹ค๋ฅธ ๋‘ run ์ด ์‚ฌ์‹ค ๊ฐ™์€ ์„ค์ •" ์ธ์ง€๋ฅผ ์‚ฌ๋žŒ์ด ํ•ด์‹œ๋ฅผ ๋– ์„œ ๋Œ€์กฐํ•ด์•ผ ํ–ˆ๋‹ค
6
+ (`insights/260803_etd_regression_prod_vs_v24.md` ์˜ '์ด๋ฆ„ ๋งคํ•‘' ์ ˆ์ด ๊ทธ ํ”์ ์ด๋‹ค).
7
  run ์— ์ง€๋ฌธ์„ ์ฐ์–ด ๋‘๋ฉด ๊ทธ ๋Œ€์กฐ๊ฐ€ ์กฐํšŒ ํ•œ ๋ฒˆ์œผ๋กœ ๋๋‚œ๋‹ค.
8
 
9
  ์ง€๋ฌธ์€ **ํ•ด์„์ด ๋๋‚œ ์„ค์ •**์„ ๋Œ€์ƒ์œผ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค. `extends`/override ๋กœ ์กฐ๋ฆฝํ•œ
 
16
 
17
  import hashlib
18
  import json
19
+ from collections.abc import Iterator, Mapping, Sequence
20
  from pathlib import Path
21
  from typing import Any
22
 
 
84
  return None
85
 
86
 
87
+ def iter_prompt_asset_files(root: Path) -> Iterator[Path]:
88
+ """ํ”„๋กฌํ”„ํŠธ ๋””๋ ‰ํ† ๋ฆฌ์—์„œ ์ง€๋ฌธ ๋Œ€์ƒ ํŒŒ์ผ์„ ๊ฒฝ๋กœ ์ •๋ ฌ ์ˆœ์„œ๋กœ ๋‚ธ๋‹ค.
89
+
90
+ ๊ทœ์น™: `PROMPT_ASSET_SUFFIXES` ํ™•์žฅ์ž(๋Œ€์†Œ๋ฌธ์ž ๋ฌด์‹œ)์˜ ์ผ๋ฐ˜ ํŒŒ์ผ, `root` ๊ธฐ์ค€ ๊ฒฝ๋กœ์˜
91
+ ์–ด๋А ๋ถ€๋ถ„์ด๋“  `.` ์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด(์ˆจ๊น€) ์ œ์™ธ. legacy `prompt_sha` ์™€ ์Šค๋ƒ…์ƒท์˜
92
+ `prompt_assets`(`core/snapshot/resources.py`)๊ฐ€ **๊ฐ™์€ ํŒŒ์ผ ์ง‘ํ•ฉ**์„ ๋ณด๋„๋ก ์ด ํ•จ์ˆ˜
93
+ ํ•˜๋‚˜๋ฅผ ๊ณต์œ ํ•œ๋‹ค(๊ณ„ํš 2026-09-24-phase1a D12).
94
+ """
95
  for file in sorted(root.rglob("*")):
96
  if not file.is_file() or file.suffix.lower() not in PROMPT_ASSET_SUFFIXES:
97
  continue
98
  if any(part.startswith(".") for part in file.relative_to(root).parts):
99
  continue
100
+ yield file
101
+
102
+
103
+ def _collect_prompt_assets(root: Path) -> dict[str, str]:
104
+ """ํ”„๋กฌํ”„ํŠธ ๋””๋ ‰ํ† ๋ฆฌ์˜ `{์ƒ๋Œ€๊ฒฝ๋กœ: ํŒŒ์ผ์ง€๋ฌธ}` ๋งต. ์ˆจ๊น€ ํŒŒ์ผ์€ ์ œ์™ธํ•œ๋‹ค."""
105
+ return {
106
+ file.relative_to(root).as_posix(): _file_digest(file)
107
+ for file in iter_prompt_asset_files(root)
108
+ }
109
 
110
 
111
  def _file_digest(path: Path) -> str:
solar_eval/core/pipeline_factory.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ํƒœ์Šคํฌ โ†’ ํŒŒ์ดํ”„๋ผ์ธ ์ƒ์„ฑ ํ•œ ๊ณณ (๊ณ„ํš D6ยท๊ฒ€ํ†  F11).
2
+
3
+ CLI ์™€ ๋Ÿฌ๋„ˆ๊ฐ€ ํŒŒ์ดํ”„๋ผ์ธ์„ ๋”ฐ๋กœ ๋งŒ๋“ค๋ฉด `project_dir`ยท`config_dir` ํ•ด์„์ด ๊ฐˆ๋ฆด ์ˆ˜ ์žˆ๋‹ค.
4
+ ๊ทธ๋ž˜์„œ ์ƒ์„ฑ ๊ทœ์น™์„ ์—ฌ๊ธฐ ํ•˜๋‚˜๋กœ ๋‘๊ณ , CLI ๋Š” ํ•œ ๋ฒˆ ๋งŒ๋“  ์ธ์Šคํ„ด์Šค๋ฅผ ์Šค๋ƒ…์ƒท
5
+ (`step_impl_hash`ยท`resources`)๊ณผ `execute_run(pipeline=...)` ์— **๊ฐ™์ด** ๋„˜๊ธด๋‹ค.
6
+ ๋Ÿฌ๋„ˆ๋Š” `pipeline` ์ธ์ž๊ฐ€ ์—†์œผ๋ฉด(์„œ๋น„์Šคยท์˜› ํ˜ธ์ถœ์ž) ์ด ํ•จ์ˆ˜๋กœ ๋งŒ๋“ ๋‹ค.
7
+
8
+ ์ด ๋ชจ๋“ˆ์€ poc-bundle ํŒจํ‚ค์ง€์™€ packaging ์„ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š”๋‹ค(D18).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+ from typing import Any, Mapping
15
+
16
+ from solar_eval.core.dataset_loader import DatasetLoader
17
+ from solar_eval.pipelines.base import BasePipeline
18
+ from solar_eval.pipelines.registry import create_pipeline
19
+
20
+
21
+ def task_project_dir(
22
+ project_config: Mapping[str, Any], dataset_loader: DatasetLoader
23
+ ) -> Path | None:
24
+ """๋ฐ์ดํ„ฐ ๋ฃจํŠธ ๊ธฐ์ค€ ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ -- v24 `tool_calling_judge` ๊ฐ€ `pmi_lookup` ์ƒ๋Œ€๊ฒฝ๋กœ๋ฅผ ํ’€ ๋•Œ ์“ด๋‹ค.
25
+
26
+ `dataset_loader.base_dir` ์ด projects ๋ฃจํŠธ์ด๋ฏ€๋กœ project ์ด๋ฆ„์„ ๋ถ™์ด๋ฉด project_dir ์ด
27
+ ๋œ๋‹ค -- CLI(`_start_local`)๊ฐ€ ์ด๋ฏธ ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ด€๋ก€(ยง5-F).
28
+ """
29
+ project_name = project_config.get("name")
30
+ if dataset_loader.base_dir and project_name:
31
+ return dataset_loader.base_dir / project_name
32
+ return None
33
+
34
+
35
+ def build_task_pipeline(
36
+ project_config: Mapping[str, Any],
37
+ task_config: Mapping[str, Any],
38
+ prompt: Mapping[str, Any],
39
+ dataset_loader: DatasetLoader,
40
+ ) -> BasePipeline:
41
+ """ํƒœ์Šคํฌ ์„ค์ •์œผ๋กœ ์‹คํ–‰ํ•  ํŒŒ์ดํ”„๋ผ์ธ์„ ๋งŒ๋“ ๋‹ค. ๋ชจ๋ฅด๋Š” ์Šคํ… ์ด๋ฆ„์€ ์—ฌ๊ธฐ์„œ ์ฃฝ๋Š”๋‹ค(run ์ „).
42
+
43
+ `config_dir` ์€ ๋ ˆํฌ ๊ด€๋ฆฌ ํ”„๋กœ์ ํŠธ๋ฉด project_loader ๊ฐ€ ์ฑ„์›Œ ๋‘” config ์ •๋ณธ ๊ฒฝ๋กœ
44
+ (03-evaluation) -- ์น˜ํ™˜ ์‚ฌ์ „ ๊ฐ™์€ ์ง€์‹ ์ž์‚ฐ์ด ์—ฌ๊ธฐ์„œ ์˜จ๋‹ค.
45
+ """
46
+ return create_pipeline(
47
+ pipeline_type=task_config.get("pipeline", "single_step"),
48
+ input_fields=task_config.get("input_fields", []),
49
+ pipeline_config=task_config.get("pipeline_config"),
50
+ prompts=prompt.get("step_prompts", {}),
51
+ dataset_loader=dataset_loader,
52
+ project_dir=task_project_dir(project_config, dataset_loader),
53
+ config_dir=project_config.get("config_dir"),
54
+ )
solar_eval/core/row_filter.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ํƒœ๊ทธ ํ–‰ ํ•„ํ„ฐ -- `where: {tags, category}` ๋ฌธ๋ฒ•๊ณผ ํ–‰ ์„ ํƒ (๊ณ„ํš D1~D3).
2
+
3
+ ๋ฌธ๋ฒ•์€ ๋‘ ํ‚ค๋กœ ๋‹ซํ˜€ ์žˆ๋‹ค:
4
+
5
+ - `tags`: **AND** -- ํ–‰์˜ `tags` ๊ฐ€ ๋‚˜์—ดํ•œ ํƒœ๊ทธ๋ฅผ ๋ชจ๋‘ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค(๋‹ค์ค‘๊ฐ’ ๋ผ๋ฒจ).
6
+ - `category`: **OR** -- ํ–‰์˜ `category`(ํ–‰๋งˆ๋‹ค ํ•˜๋‚˜)๊ฐ€ ๋ชฉ๋ก ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•œ๋‹ค.
7
+ - ๋‘˜ ๋‹ค ์žˆ์œผ๋ฉด AND.
8
+
9
+ ์„ ํƒ ์ˆœ์„œ๋Š” `where โ†’ limit(์•ž์—์„œ N๊ฐœ)` ์ด๊ณ , ์„ ํƒ๋œ ํ–‰์€ **์›๋ณธ ํŒŒ์ผ์˜ 0-based ๋ ˆ์ฝ”๋“œ
10
+ ์œ„์น˜**(`sample_idx`)๋ฅผ ๊ทธ๋Œ€๋กœ ๋“ค๊ณ  ๊ฐ„๋‹ค -- Experiment Hub ์กฐ์ธยทresumeยทeval_details ๊ฐ€ ๋ชจ๋‘ ์ด
11
+ ๋ฒˆํ˜ธ๋ฅผ ์ •๋ณธ์œผ๋กœ ์“ฐ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(D2).
12
+
13
+ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋Š” `sample_idx` ์™€ ํ‚ค ์ด๋ฆ„ยทํƒ€์ž… ์ด๋ฆ„๋งŒ ์‹ฃ๊ณ  **ํ–‰ ๋ณธ๋ฌธ์„ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค**
14
+ (๊ณ ๊ฐ ํ…์ŠคํŠธ๊ฐ€ ๋กœ๊ทธยท์˜ˆ์™ธ๋กœ ์ƒˆ์ง€ ์•Š๊ฒŒ).
15
+
16
+ ์ด ๋ชจ๋“ˆ์€ poc-bundle ํŒจํ‚ค์ง€์™€ packaging ์„ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š”๋‹ค(D18 -- HF Space ๋ฒค๋” ์‚ฌ๋ณธ).
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from typing import Any, Iterable, Mapping, Sequence
23
+
24
+ WHERE_KEYS: tuple[str, ...] = ("tags", "category")
25
+ # ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์— ์‹ฃ๋Š” ๋ฌธ์ œ ํ–‰ ๋ฒˆํ˜ธ์˜ ์ตœ๋Œ€ ๊ฐœ์ˆ˜ -- ์ „๋ถ€ ๋‚˜์—ดํ•˜๋ฉด ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฐ์ดํ„ฐ์…‹ ํฌ๊ธฐ๋งŒํผ ๊ธธ์–ด์ง„๋‹ค.
26
+ _MAX_REPORTED_IDX = 5
27
+
28
+
29
+ class SelectionError(ValueError):
30
+ """where ๋ฌธ๋ฒ• ์˜ค๋ฅ˜, ๋ผ๋ฒจ์ด ๋œ ๋ถ™์€ ๋ฐ์ดํ„ฐ์…‹, 0ํ–‰ ์„ ํƒ ๋“ฑ -- run ์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค."""
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Where:
35
+ """์ •๊ทœํ™”๋œ ํ–‰ ํ•„ํ„ฐ. ๊ฐ’์€ ์ •๋ ฌยท์ค‘๋ณต ์ œ๊ฑฐ๋œ ํŠœํ”Œ์ด๊ณ , ์ง€์ • ์•ˆ ํ•œ ํ‚ค๋Š” `None` ์ด๋‹ค."""
36
+
37
+ tags: tuple[str, ...] | None = None
38
+ category: tuple[str, ...] | None = None
39
+
40
+ def is_empty(self) -> bool:
41
+ return self.tags is None and self.category is None
42
+
43
+ def to_json(self) -> dict[str, list[str]]:
44
+ """์ •๊ทœํ™” JSON -- ํ‚ค ์ •๋ ฌ(D3: category โ†’ tags), ๊ฐ’ ์ •๋ ฌ, ์ง€์ • ์•ˆ ํ•œ ํ‚ค๋Š” ์ƒ๋žต."""
45
+ out: dict[str, list[str]] = {}
46
+ for key in sorted(WHERE_KEYS):
47
+ values = getattr(self, key)
48
+ if values is not None:
49
+ out[key] = list(values)
50
+ return out
51
+
52
+ def describe(self) -> str:
53
+ """์‚ฌ๋žŒ์šฉ ํ•œ ์ค„ ํ‘œ๊ธฐ: `category=c tags=a,b` -- ํ‚ค ์ˆœ์„œ๋Š” `to_json` ๊ณผ ๊ฐ™๋‹ค(์ •๋ ฌ)."""
54
+ return " ".join(f"{k}={','.join(v)}" for k, v in self.to_json().items())
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class RowSelection:
59
+ """run ์ด ๋Œ ํ–‰ ์ง‘ํ•ฉ์˜ ์ •์˜: `where`(์—†์œผ๋ฉด ์ „์ฒด) ๋’ค ์•ž์—์„œ `limit` ๊ฐœ."""
60
+
61
+ where: Where | None = None
62
+ limit: int | None = None
63
+
64
+ def __post_init__(self) -> None:
65
+ if self.where is not None and self.where.is_empty():
66
+ # ๋นˆ Where ์™€ None ์ด ๊ฐ™์€ ๋œป์ธ๋ฐ ์„œ๋กœ ๋‹ค๋ฅด๊ฒŒ ๋น„๊ต๋˜๋ฉด resume ๊ฐ€๋“œ๊ฐ€ ํ”๋“ค๋ฆฐ๋‹ค.
67
+ object.__setattr__(self, "where", None)
68
+ limit = self.limit
69
+ if limit is not None and (isinstance(limit, bool) or not isinstance(limit, int)):
70
+ raise SelectionError(f"limit must be a positive integer, got {type(limit).__name__}")
71
+ if limit is not None and limit < 1:
72
+ raise SelectionError(f"limit must be a positive integer, got {limit}")
73
+
74
+ def to_json(self) -> dict[str, Any]:
75
+ """run.json `dataset.selection` ์— ๊ธฐ๋กํ•˜๋Š” ์ •๊ทœํ™” ํ˜•ํƒœ."""
76
+ return {
77
+ "where": self.where.to_json() if self.where is not None else None,
78
+ "limit": self.limit,
79
+ }
80
+
81
+ @classmethod
82
+ def from_json(cls, obj: Any) -> RowSelection:
83
+ """`to_json()` ์˜ ์—ญ. resume ์ด run.json ์— ๊ธฐ๋ก๋œ ์„ ํƒ์„ ๋˜์‚ด๋ฆด ๋•Œ ์“ด๋‹ค."""
84
+ if not isinstance(obj, Mapping):
85
+ raise SelectionError(f"selection must be an object, got {type(obj).__name__}")
86
+ unknown = sorted(set(obj) - {"where", "limit"})
87
+ if unknown:
88
+ raise SelectionError(f"selection has unknown keys: {unknown}")
89
+ where = parse_where(obj.get("where"), origin="recorded selection")
90
+ return cls(where=where, limit=obj.get("limit"))
91
+
92
+
93
+ def _parse_values(key: str, raw: Any, origin: str) -> tuple[str, ...]:
94
+ """where ๊ฐ’ ํ•˜๋‚˜ ๊ฒ€์ฆ: ๋น„์–ด ์žˆ์ง€ ์•Š์€ ๋ฌธ์ž์—ด์˜ ๋น„์–ด ์žˆ์ง€ ์•Š์€ ๋ฆฌ์ŠคํŠธ, ์ค‘๋ณต ๋ถˆ๊ฐ€.
95
+
96
+ ๊ฐ’์€ ์•ž๋’ค ๊ณต๋ฐฑ์„ ๋–ผ์–ด ์ €์žฅํ•œ๋‹ค -- CLI(`parse_cli_where`)๋„ ๋–ผ๋ฏ€๋กœ YAML ๊ณผ CLI ๊ฐ€ ๊ฐ™์€
97
+ ๊ฐ’์œผ๋กœ ์ •๊ทœํ™”๋œ๋‹ค. ์ค‘๋ณต ๊ฒ€์‚ฌ๋„ ๋—€ ๊ฐ’์œผ๋กœ ํ•œ๋‹ค.
98
+ """
99
+ if not isinstance(raw, list) or not raw:
100
+ raise SelectionError(f"{origin}: where.{key} must be a non-empty list of strings")
101
+ for value in raw:
102
+ if not isinstance(value, str) or not value.strip():
103
+ raise SelectionError(f"{origin}: where.{key} values must be non-empty strings")
104
+ values = [value.strip() for value in raw]
105
+ duplicates = sorted({v for v in values if values.count(v) > 1})
106
+ if duplicates:
107
+ raise SelectionError(f"{origin}: where.{key} has duplicate values: {duplicates}")
108
+ return tuple(sorted(values))
109
+
110
+
111
+ def parse_where(obj: Any, origin: str) -> Where | None:
112
+ """`project.yaml` ๏ฟฝ๏ฟฝ์Šคํฌ์˜ `where` ๊ฐ์ฒด(๋˜๋Š” ๊ธฐ๋ก๋œ ์„ ํƒ)๋ฅผ ๊ฒ€์ฆํ•ด `Where` ๋กœ ๋งŒ๋“ ๋‹ค.
113
+
114
+ `None` ์€ "ํ•„ํ„ฐ ์—†์Œ". ๋ชจ๋ฅด๋Š” ํ‚คยท๋นˆ ๋ฆฌ์ŠคํŠธยท์ค‘๋ณต์€ ์˜ค๋ฅ˜๋‹ค(๋ฌธ๋ฒ•์ด ๋‹ซํ˜€ ์žˆ์–ด์•ผ ์˜คํƒ€๊ฐ€
115
+ ์กฐ์šฉํžˆ 0ํ–‰์ด ๋˜์ง€ ์•Š๋Š”๋‹ค).
116
+ """
117
+ if obj is None:
118
+ return None
119
+ if not isinstance(obj, Mapping):
120
+ raise SelectionError(f"{origin}: where must be an object, got {type(obj).__name__}")
121
+ unknown = sorted(str(k) for k in set(obj) - set(WHERE_KEYS))
122
+ if unknown:
123
+ raise SelectionError(
124
+ f"{origin}: where has unknown keys {unknown} (allowed: tags, category)"
125
+ )
126
+ if not obj:
127
+ raise SelectionError(f"{origin}: where is empty -- omit it instead")
128
+ parsed = {key: _parse_values(key, obj[key], origin) for key in WHERE_KEYS if key in obj}
129
+ return Where(**parsed)
130
+
131
+
132
+ def parse_cli_where(values: Iterable[str]) -> Where | None:
133
+ """CLI `--where key=v1,v2` (๋ฐ˜๋ณต ๊ฐ€๋Šฅ)๋ฅผ ํ•˜๋‚˜์˜ `Where` ๋กœ ์ ‘๋Š”๋‹ค.
134
+
135
+ ๋ฐ˜๋ณต์€ AND ๋‹ค: `tags` ๋Š” ํ•ฉ์ง‘ํ•ฉ(๋ชจ๋‘ ๊ฐ€์ ธ์•ผ ํ•จ), `category` ๋Š” ๊ต์ง‘ํ•ฉ -- `combine` ๊ณผ
136
+ ๊ฐ™์€ ๊ทœ์น™์ด๋‹ค.
137
+ """
138
+ result: Where | None = None
139
+ for raw in values:
140
+ key, sep, rest = raw.partition("=")
141
+ key = key.strip()
142
+ if not sep:
143
+ raise SelectionError(f"--where {raw!r}: expected key=value1,value2")
144
+ if key not in WHERE_KEYS:
145
+ raise SelectionError(f"--where {raw!r}: unknown key {key!r} (allowed: tags, category)")
146
+ items = [part.strip() for part in rest.split(",")]
147
+ if not rest.strip() or any(not part for part in items):
148
+ raise SelectionError(f"--where {raw!r}: empty value")
149
+ one = parse_where({key: items}, origin=f"--where {raw!r}")
150
+ result = combine(result, one)
151
+ return result
152
+
153
+
154
+ def combine(task: Where | None, cli: Where | None) -> Where | None:
155
+ """ํƒœ์Šคํฌ where ์™€ CLI where ๋ฅผ AND ๋กœ ํ•ฉ์นœ๋‹ค.
156
+
157
+ `tags` ๋Š” ํ•ฉ์ง‘ํ•ฉ(๋ชจ๋‘ ๊ฐ€์ ธ์•ผ ํ•จ), `category` ๋Š” ๊ต์ง‘ํ•ฉ(๋‘˜ ๋‹ค ์žˆ์„ ๋•Œ). ๊ต์ง‘ํ•ฉ์ด ๋น„๋ฉด
158
+ ์–ด๋–ค ํ–‰๋„ ๊ณ ๋ฅผ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ run ์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ์‹คํŒจํ•œ๋‹ค.
159
+ """
160
+ if task is None or task.is_empty():
161
+ return cli if cli is not None and not cli.is_empty() else None
162
+ if cli is None or cli.is_empty():
163
+ return task
164
+ tags = _merge_optional(task.tags, cli.tags, lambda a, b: a | b)
165
+ category = _merge_optional(task.category, cli.category, lambda a, b: a & b)
166
+ if category is not None and not category:
167
+ raise SelectionError(
168
+ f"where category has no overlap: {list(task.category or ())} "
169
+ f"vs {list(cli.category or ())}"
170
+ )
171
+ return Where(tags=tags, category=category)
172
+
173
+
174
+ def _merge_optional(a, b, op) -> tuple[str, ...] | None:
175
+ if a is None:
176
+ return b
177
+ if b is None:
178
+ return a
179
+ return tuple(sorted(op(set(a), set(b))))
180
+
181
+
182
+ def format_indices(indices: Sequence[int]) -> str:
183
+ """์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์šฉ `sample_idx 1, 4, ... (+N more)` -- ํ–‰ ๋ณธ๋ฌธ ์—†์ด ๋ฒˆํ˜ธ๋งŒ."""
184
+ shown = ", ".join(str(i) for i in indices[:_MAX_REPORTED_IDX])
185
+ more = (
186
+ f" (+{len(indices) - _MAX_REPORTED_IDX} more)" if len(indices) > _MAX_REPORTED_IDX else ""
187
+ )
188
+ return f"sample_idx {shown}{more}"
189
+
190
+
191
+ def _check_labels(records: Sequence[Any], where: Where) -> None:
192
+ """where ๊ฐ€ ๋ณด๋Š” ํ‚ค๊ฐ€ ๋ชจ๋“  ํ–‰์— ์˜ฌ๋ฐ”๋ฅธ ํƒ€์ž…์œผ๋กœ ์žˆ๋Š”์ง€ -- ๋œ ๋ถ™์€ ๋ผ๋ฒจ์„ ์กฐ์šฉํžˆ ์ž๋ฅด์ง€ ์•Š๋Š”๋‹ค."""
193
+ problems: dict[str, list[int]] = {}
194
+ for idx, row in enumerate(records):
195
+ if not isinstance(row, Mapping):
196
+ problems.setdefault("row is not an object", []).append(idx)
197
+ continue
198
+ if where.tags is not None:
199
+ _check_tags(row, idx, problems)
200
+ if where.category is not None:
201
+ _check_category(row, idx, problems)
202
+ if problems:
203
+ details = "; ".join(
204
+ f"{reason} at {format_indices(idx)}" for reason, idx in problems.items()
205
+ )
206
+ raise SelectionError(
207
+ f"dataset rows cannot be filtered by where {where.describe()}: {details}"
208
+ )
209
+
210
+
211
+ def _check_tags(row: Mapping[str, Any], idx: int, problems: dict[str, list[int]]) -> None:
212
+ if "tags" not in row:
213
+ problems.setdefault("missing 'tags'", []).append(idx)
214
+ elif not isinstance(row["tags"], list):
215
+ problems.setdefault("'tags' is not a list", []).append(idx)
216
+ elif not all(isinstance(t, str) for t in row["tags"]):
217
+ problems.setdefault("'tags' has a non-string element", []).append(idx)
218
+
219
+
220
+ def _check_category(row: Mapping[str, Any], idx: int, problems: dict[str, list[int]]) -> None:
221
+ if "category" not in row:
222
+ problems.setdefault("missing 'category'", []).append(idx)
223
+ elif row["category"] is not None and not isinstance(row["category"], str):
224
+ # `category: null`(apply --lenient ๊ฐ€ ๋‚จ๊ธฐ๋Š” ํ–‰)์€ ํƒ€์ž… ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹ˆ๋ผ ๋ถˆ์ผ์น˜๋‹ค.
225
+ problems.setdefault("'category' is not a string", []).append(idx)
226
+
227
+
228
+ def _absent_values(records: Sequence[Mapping[str, Any]], where: Where) -> dict[str, list[str]]:
229
+ """where ๊ฐ’ ์ค‘ ๋ฐ์ดํ„ฐ์…‹์˜ **์–ด๋А ํ–‰์—๋„** ์—†๋Š” ๊ฐ’ -- ์˜คํƒ€ยท๋Œ€์†Œ๋ฌธ์ž ์ฐจ์ด๋ฅผ ์žก๋Š”๋‹ค.
230
+
231
+ ํ•„ํ„ฐ ๊ฒฐ๊ณผ๊ฐ€ ์•„๋‹ˆ๋ผ ํŒŒ์ผ ์ „์ฒด์™€ ๋Œ€์กฐํ•œ๋‹ค: `tags=a, category=x,y` ์—์„œ `y` ๊ฐ€ ํŒŒ์ผ์—” ์žˆ์ง€๋งŒ
232
+ `a` ํƒœ๊ทธ ํ–‰์—๋งŒ ์—†๋Š” ๊ฒƒ์€ ์ •๋‹นํ•œ ๋นˆ ์กฐํ•ฉ์ด์ง€ ์˜คํƒ€๊ฐ€ ์•„๋‹ˆ๋‹ค. ๋ผ๋ฒจ ๊ฒ€์‚ฌ(`_check_labels`)๋ฅผ
233
+ ํ†ต๊ณผํ•œ ๋’ค์— ๋ถ€๋ฅด๋ฏ€๋กœ ํƒ€์ž…์€ ์ด๋ฏธ ๋งž๋‹ค.
234
+ """
235
+ absent: dict[str, list[str]] = {}
236
+ if where.category is not None:
237
+ present = {row["category"] for row in records}
238
+ missing = [v for v in where.category if v not in present]
239
+ if missing:
240
+ absent["category"] = missing
241
+ if where.tags is not None:
242
+ present = {tag for row in records for tag in row["tags"]}
243
+ missing = [v for v in where.tags if v not in present]
244
+ if missing:
245
+ absent["tags"] = missing
246
+ return absent
247
+
248
+
249
+ def _describe_absent(absent: dict[str, list[str]]) -> str:
250
+ return "; ".join(f"{key} {values}" for key, values in absent.items())
251
+
252
+
253
+ def _matches(row: Mapping[str, Any], where: Where) -> bool:
254
+ if where.tags is not None and not set(where.tags).issubset(row["tags"]):
255
+ return False
256
+ return where.category is None or row["category"] in where.category
257
+
258
+
259
+ def select(
260
+ records: Sequence[dict[str, Any]], selection: RowSelection | None
261
+ ) -> list[tuple[int, dict[str, Any]]]:
262
+ """์„ ํƒ์„ ์ ์šฉํ•ด `(์›๋ณธ idx, ํ–‰)` ๋ชฉ๋ก์„ ๋Œ๋ ค์ค€๋‹ค. ์ˆœ์„œ๋Š” ์›๋ณธ ์ˆœ์„œ.
263
+
264
+ `selection` ์ด `None` ์ด๋ฉด ์ „์ฒด. where ๊ฐ€ ์žˆ๋Š”๋ฐ 0ํ–‰์ด๋ฉด `SelectionError`
265
+ (์กฐ์šฉํžˆ ๋นˆ run ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค). where ์—†์ด ๋นˆ ํŒŒ์ผ์ธ ๊ฒฝ์šฐ๋Š” ์˜ค๋Š˜ ๋™์ž‘ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค.
266
+ where ๊ฐ’ ์ค‘ ํŒŒ์ผ์˜ ์–ด๋А ํ–‰์—๋„ ์—†๋Š” ๊ฐ’์ด ์žˆ์–ด๋„ `SelectionError` ๋‹ค -- `category` ๋Š”
267
+ OR ๋ผ ์˜คํƒ€ ํ•˜๋‚˜๊ฐ€ 0ํ–‰์ด ์•„๋‹ˆ๋ผ "์กฐ์šฉํžˆ ์ค„์–ด๋“  ์„ ํƒ"์ด ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(D1 ์ด ํ‘œํ˜„์‹
268
+ ๋ฌธ์ž์—ด์„ ๋ฒ„๋ฆฐ ๊ฒƒ๊ณผ ๊ฐ™์€ ์ด์œ ).
269
+ """
270
+ indexed = list(enumerate(records))
271
+ if selection is None:
272
+ return indexed
273
+ where = selection.where
274
+ if where is not None:
275
+ _check_labels(records, where)
276
+ absent = _absent_values(records, where)
277
+ indexed = [(idx, row) for idx, row in indexed if _matches(row, where)]
278
+ if not indexed:
279
+ hint = f" (not in any row: {_describe_absent(absent)})" if absent else ""
280
+ raise SelectionError(f"where {where.describe()} matched 0 of {len(records)} rows{hint}")
281
+ if absent:
282
+ raise SelectionError(
283
+ f"where value(s) not in any of {len(records)} rows: {_describe_absent(absent)} "
284
+ "-- check spelling/case, or drop them"
285
+ )
286
+ if selection.limit is not None:
287
+ indexed = indexed[: selection.limit]
288
+ return indexed
solar_eval/core/run_records.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """run ์‚ฐ์ถœ๋ฌผ ๋ ˆ์ฝ”๋“œ ์กฐ๋ฆฝ -- results ํ–‰ ยท eval_details ํ–‰ ยท evaluation ๋ฌธ์„œ.
2
+
3
+ ๋Ÿฌ๋„ˆ(`execute_run`)์™€ `runs eval`(์žฌ์ฑ„์ ) CLI ๊ฐ€ ๊ฐ™์€ ๋ชจ์–‘์„ ์“ฐ๋„๋ก ํ•œ ๊ณณ์— ๋‘”๋‹ค
4
+ (์˜ˆ์ „์—” ๋‘ ๊ณณ์— ๋ณต๋ถ™๋ผ ์žˆ์—ˆ๋‹ค). ์ €์žฅ ํ‚ค ์ด๋ฆ„์€ eval-store ์˜ `resultRowSchema`ยท
5
+ `evaluationJsonSchema` ์™€ ํ•˜์œ„ ํ˜ธํ™˜์ด์–ด์•ผ ํ•˜๋ฏ€๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค.
6
+
7
+ `evaluation` ๋ฌธ์„œ์˜ `eval_model` ์€ **์ธ์ž**๋‹ค -- ์–ด๋–ค judge ๋ชจ๋ธ๋กœ ์ฑ„์ ํ–ˆ๋Š”์ง€๋Š”
8
+ ํ˜ธ์ถœ์ž(ํ‰๊ฐ€๊ธฐ)๊ฐ€ ์•ˆ๋‹ค. ์ด ๋ชจ๋“ˆ์ด ๊ฐ’์„ ์ง€์–ด๋‚ด์ง€ ์•Š๋Š”๋‹ค.
9
+
10
+ ์ด ๋ชจ๋“ˆ์€ poc-bundle ํŒจํ‚ค์ง€์™€ packaging ์„ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๋Š”๋‹ค(D18).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Mapping, Sequence
16
+
17
+ from solar_eval.models.sample import EvalSample
18
+
19
+ #: run.json ์—์„œ ์ถ”๋ก  ์‹คํŒจ ์ƒ˜ํ”Œ์˜ ์˜ˆ์™ธ ํƒ€์ž… ๋ชฉ๋ก์ด ์‚ฌ๋Š” ํ‚ค(LP5 D-LP5-4). ์ด๋ฆ„์˜ ์ •๋ณธ์€ ์—ฌ๊ธฐ ํ•œ ๊ณณ์ด๋‹ค --
20
+ #: CLI ์˜ ์‹คํŒจ ์ง€๋ฌธ ๋„์šฐ๋ฏธ(`cli/commands/failure_keys.py`)๋„ ์ด ์ƒ์ˆ˜๋กœ ์ฝ๋Š”๋‹ค.
21
+ FAILED_SAMPLE_ERRORS_KEY = "failed_sample_errors"
22
+
23
+
24
+ def build_failure_errors(errors: Sequence[tuple[int, BaseException]]) -> list[dict[str, Any]]:
25
+ """์ถ”๋ก  ์‹คํŒจ `(์›๋ณธ sample_idx, ์˜ˆ์™ธ)` ๋ชฉ๋ก โ†’ run.json `failed_sample_errors` ์›์†Œ๋“ค.
26
+
27
+ ์›์†Œ๋Š” `{"sample_idx", "error_type"}` ๋‘ ํ‚ค์ด๊ณ  `error_type` ์€ ์˜ˆ์™ธ **ํƒ€์ž… ์ด๋ฆ„**๋ฟ์ด๋‹ค --
28
+ ๋ฉ”์‹œ์ง€๋Š” ์ž…๋ ฅ ์กฐ๊ฐ์„ ๋‹ด์„ ์ˆ˜ ์žˆ์–ด ์‹ฃ์ง€ ์•Š๋Š”๋‹ค. `sample_idx` ์˜ค๋ฆ„์ฐจ์ˆœ, ์‹คํŒจ๊ฐ€ ์—†์œผ๋ฉด `[]`.
29
+ """
30
+ ordered = sorted(errors, key=lambda pair: pair[0])
31
+ return [{"sample_idx": idx, "error_type": type(exc).__name__} for idx, exc in ordered]
32
+
33
+
34
+ def extract_input(sample: Mapping[str, Any], input_fields: Sequence[str]) -> dict[str, Any]:
35
+ """๋ฐ์ดํ„ฐ ํ–‰์—์„œ `input_fields` ๋งŒ ๊ณจ๋ผ ์ž…๋ ฅ dict ๋ฅผ ๋งŒ๋“ ๋‹ค(์—†๋Š” ํ‚ค๋Š” ๋นˆ ๋ฌธ์ž์—ด)."""
36
+ return {field: sample.get(field, "") for field in input_fields}
37
+
38
+
39
+ def extract_golden(sample: Mapping[str, Any], task_config: Mapping[str, Any]) -> Any:
40
+ """๋ฐ์ดํ„ฐ ํ–‰์—์„œ ์ •๋‹ต(golden)์„ ๊บผ๋‚ธ๋‹ค -- `golden_field` ์šฐ์„ , ๋‹ค์Œ `golden_fields`."""
41
+ golden_field = task_config.get("golden_field")
42
+ golden_fields = task_config.get("golden_fields")
43
+ if golden_field:
44
+ return sample.get(golden_field, "")
45
+ if golden_fields:
46
+ return {k: sample.get(v, "") for k, v in golden_fields.items()}
47
+ return ""
48
+
49
+
50
+ def build_eval_sample_from_result(result: dict[str, Any], reference: Any) -> EvalSample:
51
+ """์ €์žฅ๋œ ๊ฒฐ๊ณผ dict(๋ ˆ๊ฑฐ์‹œ 8+trace ํ‚ค)์—์„œ ์ฑ„์ ์šฉ `EvalSample` ์„ ๋˜์‚ด๋ฆฐ๋‹ค.
52
+
53
+ `insert_run_result` ๊ฐ€ ์“ฐ๋Š” ํ‚ค ์ด๋ฆ„(`golden`/`trace`)์€ `EvalSample` ํ•„๋“œ ์ด๋ฆ„
54
+ (`reference`/`artifacts`)๊ณผ ๋‹ค๋ฅด๋‹ค -- eval-store ํ•˜์œ„ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ €์žฅ ์Šคํ‚ค๋งˆ๋Š”
55
+ ๊ทธ๋Œ€๋กœ ๋‘๊ณ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš ยง9-1) ์ฑ„์  ์ง์ „์—๋งŒ ์—ฌ๊ธฐ์„œ ๋˜๋Œ๋ฆฐ๋‹ค. `execute_run`
56
+ ๋‚ด๋ถ€ ๋ฃจํ”„์™€ `runs eval` CLI(์žฌ์ฑ„์ , ์ƒˆ ์ถ”๋ก  ์—†์ด ๋””์Šคํฌ์˜ `results.jsonl` ์„
57
+ ๊ทธ๋Œ€๋กœ ์”€) ์–‘์ชฝ์—์„œ ๊ณต์œ ํ•œ๋‹ค.
58
+
59
+ Args:
60
+ result: `insert_run_result` ์— ๋„˜๊ธด ๊ฒƒ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ(๋˜๋Š” `JsonlStore.
61
+ load_existing_results()` ๋กœ ๋””์Šคํฌ์—์„œ ๋‹ค์‹œ ์ฝ์€ ๊ฒƒ). `input`/`output`/
62
+ `trace` ํ‚ค๋ฅผ ์ฝ๋Š”๋‹ค.
63
+ reference: ์ •๋‹ต(golden) ๊ฐ’. ํ˜ธ์ถœ์ž๊ฐ€ ์ง์ ‘ ๋„˜๊ธด๋‹ค -- ๋‘ ํ˜ธ์ถœ์ž์˜ golden
64
+ ์ถ”์ถœ ๊ฒฝ๋กœ๊ฐ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(์•„๋ž˜ ์ฐธ๊ณ ). ์ด ํ•จ์ˆ˜๋Š” ์–ด๋А ์ชฝ์ธ์ง€ ๋ชจ๋ฅธ ์ฑ„
65
+ ๊ฐ’๋งŒ ๋ฐ›์•„ ๊ทธ๋Œ€๋กœ `sample.reference` ์— ์ฑ„์šด๋‹ค.
66
+
67
+ - `execute_run` ๋‚ด๋ถ€ ๋ฃจํ”„: `_golden_raw`(๋””์Šคํฌ์— ์•ˆ ๋‚จ๋Š” ์‹คํ–‰ ์ค‘
68
+ ์ž„์‹œ ํ‚ค, resume ์‹œ์—๋„ ๋งค ์ƒ˜ํ”Œ๋งˆ๋‹ค ์ƒˆ๋กœ ๊ณ„์‚ฐ๋จ)๋ฅผ ๋„˜๊ธด๋‹ค.
69
+ - `runs eval` CLI: ๋””์Šคํฌ์— ์ €์žฅ๋œ `golden` ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ๋„˜๊ธด๋‹ค
70
+ (`_golden_raw` ๋Š” ์• ์ดˆ์— ์ €์žฅ๋˜์ง€ ์•Š์•„ CLI ์ชฝ์—” ์—†๋‹ค).
71
+
72
+ Returns:
73
+ `input`/`output`/`reference`/`artifacts` ๊ฐ€ ์ฑ„์›Œ์ง„ `EvalSample`.
74
+ `contexts` ๋Š” ์•„์ง ์•„๋ฌด๋„ ์•ˆ ์จ์„œ ๊ธฐ๋ณธ๊ฐ’(`None`) ๊ทธ๋Œ€๋กœ๋‹ค.
75
+ """
76
+ trace = result.get("trace") or {}
77
+ return EvalSample(
78
+ input=result.get("input"),
79
+ output=result.get("output"),
80
+ reference=reference,
81
+ artifacts=trace,
82
+ )
83
+
84
+
85
+ def _usage_mapping(usage: Any) -> Mapping[str, Any]:
86
+ """artifacts ์˜ `usage` ๋ฅผ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” dict ๋กœ -- dict ๊ฐ€ ์•„๋‹ˆ๋ฉด(์—†์Œยท`None` ํฌํ•จ) ๋นˆ dict.
87
+
88
+ ๋นˆ dict ์—์„œ ๊บผ๋‚ธ ํ† ํฐ ์นธ์€ `None` ์ด ๋œ๋‹ค(์ธก์ • ์—†์Œ). ์˜ˆ์ „์—๋Š” `usage: None` ์ธ ์ƒ˜ํ”Œ์ด
89
+ `None.get` ์œผ๋กœ ํ–‰ ์กฐ๋ฆฝ ์ž์ฒด๋ฅผ ์ฃฝ์˜€๋‹ค.
90
+ """
91
+ return usage if isinstance(usage, Mapping) else {}
92
+
93
+
94
+ def build_result_row(
95
+ run_id: str,
96
+ sample_idx: int,
97
+ input_data: dict[str, Any],
98
+ golden: Any,
99
+ eval_sample: EvalSample,
100
+ meta: dict[str, Any] | None = None,
101
+ ) -> dict[str, Any]:
102
+ """results.jsonl ํ•œ ํ–‰. ์˜ค๋Š˜์˜ 8ํ‚ค + `trace`, ๊ทธ๋ฆฌ๊ณ  `meta_fields` ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ `meta`.
103
+
104
+ `sample_idx` ๋Š” **์›๋ณธ ํŒŒ์ผ์˜ 0-based ๋ ˆ์ฝ”๋“œ ์œ„์น˜**๋‹ค(ํ•„ํ„ฐยทlimit ๋’ค์—๋„, D2).
105
+
106
+ trace ๋Š” artifacts ์ „์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค -- step_outputs ๋ฟ ์•„๋‹ˆ๋ผ v24 ๊ฐ€ ์ฑ„์šฐ๋Š”
107
+ judge_decisions/judge_tool_calls/self_consistency_runs/corrections ๋„ ์—ฌ๊ธฐ ์•ˆ ๋„ฃ์œผ๋ฉด
108
+ ์ €์žฅ ์ง์ „์— ํ†ต์งธ๋กœ ๋ฒ„๋ ค์ง„๋‹ค (์‹ค์ œ judge LLM ํ˜ธ์ถœยทself-consistency ๋ฐ˜๋ณต ํ˜ธ์ถœ ๋น„์šฉ์ด
109
+ ๋‚˜๊ฐ„ ์‚ฐ์ถœ๋ฌผ์ด๋‹ค). ํŠน์ • ํ‚ค๋งŒ ํ•˜๋“œ์ฝ”๋”ฉํ•ด ์˜ฎ๊ธฐ๋ฉด ๋‹ค์Œ์— ํŒŒ์ดํ”„๋ผ์ธ์ด ์ƒˆ artifacts ํ‚ค๋ฅผ
110
+ ์ถ”๊ฐ€ํ•  ๋•Œ๋งˆ๋‹ค ์—ฌ๊ธฐ๋ฅผ ๋˜ ๊ณ ์ณ์•ผ ํ•˜๋ฏ€๋กœ ํ†ต์งธ๋กœ ๋„˜๊ธด๋‹ค -- resultRowSchema.trace ๋Š”
111
+ .passthrough() ๋ผ ์—ฌ๋ถ„ ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ›๋Š”๋‹ค (source-schemas.ts:223-228).
112
+
113
+ `meta` ๋Š” input ์ด ์•„๋‹ˆ๋‹ค -- ๋ฐ์ดํ„ฐ ํ–‰์˜ ํƒœ๊ทธยท์ถœ์ฒ˜ ๋ณต์‚ฌ๋ณธ์ด๊ณ (D13), `meta_fields`
114
+ ์„ ์–ธ์ด ์—†์œผ๋ฉด ํ‚ค ์ž์ฒด๋ฅผ ์“ฐ์ง€ ์•Š์•„ ๋‹ค๋ฅธ ํ”„๋กœ์ ํŠธ์˜ results ๋ฐ”์ดํŠธ๊ฐ€ ๊ทธ๋Œ€๋กœ๋‹ค.
115
+
116
+ ์‚ฌ์šฉ๋Ÿ‰ยท์ง€์—ฐ(`input_tokens`ยท`output_tokens`ยท`inference_time_ms`)์€ artifacts ์— ์—†์œผ๋ฉด
117
+ `None` ์ด๋‹ค(L6b B1 ยท D-M2-24) -- `0` ์„ ์ง€์–ด ๋„ฃ์œผ๋ฉด "์ธก์ • ์—†์Œ" ์ด "0 ์„ ์ผ๋‹ค" ๋Š”
118
+ ์ธก์ •๊ฐ’๊ณผ ๊ตฌ๋ถ„๋˜์ง€ ์•Š๋Š”๋‹ค. ๊ฐ’์ด ์žˆ์œผ๋ฉด ๊ทธ๋Œ€๋กœ ์˜ฎ๊ธด๋‹ค(`0` ๋„ ๊ทธ๋Œ€๋กœ) -- ์‚ฌ์šฉ๋Ÿ‰์ด ์žˆ๋Š”
119
+ ํ–‰์˜ ๋ฐ”์ดํŠธ๋Š” ์˜ˆ์ „๊ณผ ๊ฐ™๋‹ค.
120
+ """
121
+ usage = _usage_mapping(eval_sample.artifacts.get("usage"))
122
+ row: dict[str, Any] = {
123
+ "run_id": run_id,
124
+ "sample_idx": sample_idx,
125
+ "input": input_data,
126
+ "output": eval_sample.output,
127
+ "golden": golden,
128
+ "input_tokens": usage.get("prompt_tokens"),
129
+ "output_tokens": usage.get("completion_tokens"),
130
+ "inference_time_ms": eval_sample.artifacts.get("inference_time_ms"),
131
+ "trace": {
132
+ "step_outputs": {},
133
+ **eval_sample.artifacts,
134
+ },
135
+ }
136
+ if meta is not None:
137
+ row["meta"] = meta
138
+ return row
139
+
140
+
141
+ def build_evaluation_doc(
142
+ run_id: str,
143
+ aggregated: Mapping[str, Any],
144
+ eval_results: Sequence[Mapping[str, Any]],
145
+ eval_failures: Sequence[Mapping[str, Any]],
146
+ eval_model: str | None,
147
+ ) -> dict[str, Any]:
148
+ """evaluation.json ๋ฌธ์„œ.
149
+
150
+ ์ฑ„์ ์— ์„ฑ๊ณตํ•œ ์ƒ˜ํ”Œ์ด ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์ ์ˆ˜ ์ž๋ฆฌ๋ฅผ ๋น„์šด๋‹ค -- aggregate() ๋Š” ๋นˆ ์ž…๋ ฅ์—
151
+ 0.0 ์„ ๋Œ๋ ค์ฃผ๋Š”๋ฐ, ๊ทธ๊ฑด "0์ ์„ ๋ฐ›์•˜๋‹ค"๋Š” ์ธก์ •๊ฐ’์ด๋ผ "์ธก์ • ์ž์ฒด๊ฐ€ ์—†์—ˆ๋‹ค"์™€ ๊ตฌ๋ถ„๋˜์ง€
152
+ ์•Š๋Š”๋‹ค. judge ๊ฐ€ ํ†ต์งธ๋กœ ์ฃฝ์€ run ์ด Experiment Hub ์ฐจํŠธ์—์„œ ํ’ˆ์งˆ ๊ธ‰๋ฝ์œผ๋กœ ๋ณด์ด๋ฉด ์•ˆ ๋œ๋‹ค.
153
+ """
154
+ overall_score = aggregated.get("overall_score", 0.0) if eval_results else None
155
+ return {
156
+ "run_id": run_id,
157
+ "scores": aggregated.get("scores", {}),
158
+ "overall_score": overall_score,
159
+ "eval_model": eval_model,
160
+ "eval_success_count": len(eval_results),
161
+ "eval_failed_count": len(eval_failures),
162
+ "failed_sample_indices": [f["sample_idx"] for f in eval_failures],
163
+ }
164
+
165
+
166
+ def build_eval_detail_docs(
167
+ evaluation_id: Any,
168
+ eval_results: Sequence[Mapping[str, Any]],
169
+ eval_failures: Sequence[Mapping[str, Any]],
170
+ ) -> list[dict[str, Any]]:
171
+ """eval_details.jsonl ํ–‰๋“ค -- ์„ฑ๊ณต/์‹คํŒจ ๋‘˜ ๋‹ค ํ•œ ํ–‰์”ฉ ๋‚จ๊ธด๋‹ค.
172
+
173
+ results.jsonl ๊ณผ eval_details.jsonl ์ด ํ•ญ์ƒ ๊ฐ™์€ sample_idx ์ง‘ํ•ฉ์„ ๊ฐ€๋ฆฌํ‚ค๊ฒŒ ํ•ด์„œ
174
+ ๋ถ€๋ถ„์ ์œผ๋กœ๋งŒ ์ฑ„์ ๋œ run ์—์„œ "์ด ์ƒ˜ํ”Œ์€ ์™œ ์•ˆ ๋ณด์ด์ง€"๋ฅผ ์—†์•ค๋‹ค.
175
+ """
176
+ docs: list[dict[str, Any]] = []
177
+ for er in eval_results:
178
+ docs.append(
179
+ {
180
+ "evaluation_id": evaluation_id,
181
+ "sample_idx": er["sample_idx"],
182
+ "category_scores": er.get("category_scores", {}),
183
+ "error_counts": {
184
+ k: v.get("error_count", 0)
185
+ for k, v in er.get("details", {}).items()
186
+ if isinstance(v, dict)
187
+ },
188
+ "severity": er.get("severity", ""),
189
+ "score": er.get("score", 0.0),
190
+ }
191
+ )
192
+ for f in eval_failures:
193
+ docs.append(
194
+ {
195
+ "evaluation_id": evaluation_id,
196
+ "sample_idx": f["sample_idx"],
197
+ "category_scores": {},
198
+ "error_counts": {},
199
+ "severity": None,
200
+ # 0.0 ์ด ์•„๋‹ˆ๋ผ None -- ์ฑ„์  ์‹คํŒจ๋ฅผ ์ตœ์ € ์ ์ˆ˜์™€ ๊ตฌ๋ถ„ํ•œ๋‹ค (F1).
201
+ "score": None,
202
+ "error": f["error"],
203
+ }
204
+ )
205
+ return docs
solar_eval/core/runner.py CHANGED
@@ -1,6 +1,7 @@
1
  import asyncio
2
  import logging
3
  import sys
 
4
  from datetime import datetime, timezone
5
  from typing import Any, Callable
6
 
@@ -8,51 +9,106 @@ from typing import Any, Callable
8
  logging.basicConfig(stream=sys.stderr, level=logging.INFO, format="%(message)s")
9
 
10
  from solar_eval.core.dataset_loader import DatasetLoader
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  from solar_eval.evaluators.registry import create_evaluator
12
  from solar_eval.models.enums import RunStatus
13
  from solar_eval.models.sample import EvalSample
14
- from solar_eval.pipelines.registry import create_pipeline
15
  from solar_eval.providers.base import BaseProvider
16
  from solar_eval.stores.base import ResultStore
17
 
 
 
 
 
18
  logger = logging.getLogger(__name__)
19
 
20
  OnProgress = Callable[[dict[str, Any]], Any]
21
 
 
 
22
 
23
- def build_eval_sample_from_result(result: dict[str, Any], reference: Any) -> EvalSample:
24
- """์ €์žฅ๋œ ๊ฒฐ๊ณผ dict(๋ ˆ๊ฑฐ์‹œ 8+trace ํ‚ค)์—์„œ ์ฑ„์ ์šฉ `EvalSample` ์„ ๋˜์‚ด๋ฆฐ๋‹ค.
25
-
26
- `insert_run_result` ๊ฐ€ ์“ฐ๋Š” ํ‚ค ์ด๋ฆ„(`golden`/`trace`)์€ `EvalSample` ํ•„๋“œ ์ด๋ฆ„
27
- (`reference`/`artifacts`)๊ณผ ๋‹ค๋ฅด๋‹ค -- eval-store ํ•˜์œ„ ํ˜ธํ™˜์„ ์œ„ํ•ด ์ €์žฅ ์Šคํ‚ค๋งˆ๋Š”
28
- ๊ทธ๋Œ€๋กœ ๋‘๊ณ (๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš ยง9-1) ์ฑ„์  ์ง์ „์—๋งŒ ์—ฌ๊ธฐ์„œ ๋˜๋Œ๋ฆฐ๋‹ค. `execute_run`
29
- ๋‚ด๋ถ€ ๋ฃจํ”„์™€ `runs eval` CLI(์žฌ์ฑ„์ , ์ƒˆ ์ถ”๋ก  ์—†์ด ๋””์Šคํฌ์˜ `results.jsonl` ์„
30
- ๊ทธ๋Œ€๋กœ ์”€) ์–‘์ชฝ์—์„œ ๊ณต์œ ํ•œ๋‹ค.
31
-
32
- Args:
33
- result: `insert_run_result` ์— ๋„˜๊ธด ๊ฒƒ๊ณผ ๊ฐ™์€ ํ˜•ํƒœ(๋˜๋Š” `JsonlStore.
34
- load_existing_results()` ๋กœ ๋””์Šคํฌ์—์„œ ๋‹ค์‹œ ์ฝ์€ ๊ฒƒ). `input`/`output`/
35
- `trace` ํ‚ค๋ฅผ ์ฝ๋Š”๋‹ค.
36
- reference: ์ •๋‹ต(golden) ๊ฐ’. ํ˜ธ์ถœ์ž๊ฐ€ ์ง์ ‘ ๋„˜๊ธด๋‹ค -- ๋‘ ํ˜ธ์ถœ์ž์˜ golden
37
- ์ถ”์ถœ ๊ฒฝ๋กœ๊ฐ€ ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค(์•„๋ž˜ ์ฐธ๊ณ ). ์ด ํ•จ์ˆ˜๋Š” ์–ด๋А ์ชฝ์ธ์ง€ ๋ชจ๋ฅธ ์ฑ„
38
- ๊ฐ’๋งŒ ๋ฐ›์•„ ๊ทธ๋Œ€๋กœ `sample.reference` ์— ์ฑ„์šด๋‹ค.
39
 
40
- - `execute_run` ๋‚ด๋ถ€ ๋ฃจํ”„: `_golden_raw`(๋””์Šคํฌ์— ์•ˆ ๋‚จ๋Š” ์‹คํ–‰ ์ค‘
41
- ์ž„์‹œ ํ‚ค, resume ์‹œ์—๋„ ๋งค ์ƒ˜ํ”Œ๋งˆ๋‹ค ์ƒˆ๋กœ ๊ณ„์‚ฐ๋จ)๋ฅผ ๋„˜๊ธด๋‹ค.
42
- - `runs eval` CLI: ๋””์Šคํฌ์— ์ €์žฅ๋œ `golden` ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ๋„˜๊ธด๋‹ค
43
- (`_golden_raw` ๋Š” ์• ์ดˆ์— ์ €์žฅ๋˜์ง€ ์•Š์•„ CLI ์ชฝ์—” ์—†๋‹ค).
 
 
 
44
 
45
- Returns:
46
- `input`/`output`/`reference`/`artifacts` ๊ฐ€ ์ฑ„์›Œ์ง„ `EvalSample`.
47
- `contexts` ๋Š” ์•„์ง ์•„๋ฌด๋„ ์•ˆ ์จ์„œ ๊ธฐ๋ณธ๊ฐ’(`None`) ๊ทธ๋Œ€๋กœ๋‹ค.
 
48
  """
49
- trace = result.get("trace") or {}
50
- return EvalSample(
51
- input=result.get("input"),
52
- output=result.get("output"),
53
- reference=reference,
54
- artifacts=trace,
55
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
 
58
  class BatchRunner:
@@ -80,27 +136,31 @@ class BatchRunner:
80
  max_workers: int = 5,
81
  limit: int | None = None,
82
  completed_results: list[dict[str, Any]] | None = None,
 
 
 
83
  ) -> None:
84
  """Execute a full inference + evaluation run.
85
 
86
  Args:
87
  completed_results: Previously completed results for resume.
88
  Samples with matching sample_idx will be skipped.
 
 
 
 
 
 
 
 
89
 
90
  ์ƒ˜ํ”Œ ๋‹จ์œ„ ์‹คํŒจ(์ถ”๋ก /ํ‰๊ฐ€)๋Š” ์‚ผํ‚ค๊ณ  ์ƒํƒœ(PARTIAL/eval_failed_count)๋กœ ๊ธฐ๋กํ•˜์ง€๋งŒ,
91
- run ์„ ํ†ต์งธ๋กœ ๋ชป ๋Œ๊ฒŒ ๋งŒ๋“œ๋Š” ์˜ˆ์™ธ(๋ฐ์ดํ„ฐ์…‹ ๋กœ๋“œ ์‹คํŒจ, ํŒŒ์ดํ”„๋ผ์ธ/ํ‰๊ฐ€๊ธฐ ์ƒ์„ฑ
92
- ์‹คํŒจ, ์Šคํ† ์–ด ์“ฐ๊ธฐ ์‹คํŒจ ๋“ฑ)๋Š” ์ƒํƒœ๋ฅผ FAILED ๋กœ ๋‚จ๊ธด ๋’ค ๊ทธ๋Œ€๋กœ ์žฌ์ „ํŒŒํ•œ๋‹ค --
93
- "๋ฌด์Šจ ์ผ์ด ์žˆ์–ด๋„ ์˜ˆ์™ธ ์—†์ด ๋ฐ˜ํ™˜"์ด ์•„๋‹ˆ๋‹ค. ํ˜ธ์ถœ์ž๋Š” ์ด ํ•จ์ˆ˜๊ฐ€ raise ํ•  ์ˆ˜
94
- ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด์•ผ ํ•œ๋‹ค.
95
  """
96
  try:
97
- # Build set of already-completed sample indices for resume
98
- completed_by_idx: dict[int, dict[str, Any]] = {}
99
- if completed_results:
100
- for r in completed_results:
101
- completed_by_idx[r["sample_idx"]] = r
102
-
103
- # Update status to running
104
  await self.store.update_run(
105
  run_id,
106
  {
@@ -108,275 +168,28 @@ class BatchRunner:
108
  "started_at": datetime.now(timezone.utc),
109
  },
110
  )
111
-
112
- # Load dataset
113
- dataset_config = project_config.get("dataset", {})
114
- source = dataset_config.get("source", "huggingface")
115
- repo_name = dataset_config.get("repo", "")
116
- dataset_path = task_config.get("dataset_path", "")
117
- data = self.dataset_loader.load_jsonl(repo_name, dataset_path, source=source)
118
-
119
- if limit and limit < len(data):
120
- data = data[:limit]
121
-
122
- remaining = len(data) - len(completed_by_idx)
123
- await self.store.update_run(run_id, {"total_samples": len(data)})
124
- if on_progress:
125
- await on_progress({"type": "dataset_loaded", "total": len(data)})
126
- if completed_by_idx:
127
- logger.info(f"Resuming: {len(completed_by_idx)} done, {remaining} remaining")
128
-
129
- # Create pipeline
130
- # project_dir: v24 ์˜ tool_calling_judge ๊ฐ€ pmi_lookup ์ƒ๋Œ€๊ฒฝ๋กœ๋ฅผ ํ’€ ๋•Œ๋งŒ
131
- # ์“ด๋‹ค (ยง5-F). dataset_loader.base_dir ์ด projects ๋ฃจํŠธ์ด๋ฏ€๋กœ project ์ด๋ฆ„์„
132
- # ๋ถ™์ด๋ฉด project_dir ์ด ๋œ๋‹ค -- CLI(`_start_local`)๊ฐ€ ์ด๋ฏธ ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ๊ด€๋ก€.
133
- project_name = project_config.get("name")
134
- project_dir = (
135
- self.dataset_loader.base_dir / project_name
136
- if self.dataset_loader.base_dir and project_name
137
- else None
138
- )
139
- # config_dir: ๋ ˆํฌ ๊ด€๋ฆฌ ํ”„๋กœ์ ํŠธ๋ฉด project_loader ๊ฐ€ ์ฑ„์›Œ ๋‘” config ์ •๋ณธ
140
- # ๊ฒฝ๋กœ(03-evaluation). ์น˜ํ™˜ ์‚ฌ์ „ ๊ฐ™์€ ์ง€์‹ ์ž์‚ฐ์ด ์—ฌ๊ธฐ์„œ ์˜จ๋‹ค.
141
- pipeline = create_pipeline(
142
- pipeline_type=task_config.get("pipeline", "single_step"),
143
- input_fields=task_config.get("input_fields", []),
144
- pipeline_config=task_config.get("pipeline_config"),
145
- prompts=prompt.get("step_prompts", {}),
146
- dataset_loader=self.dataset_loader,
147
- project_dir=project_dir,
148
- config_dir=project_config.get("config_dir"),
149
- )
150
-
151
- # Run inference with concurrency control
152
- semaphore = asyncio.Semaphore(max_workers)
153
- completed = len(completed_by_idx)
154
-
155
- async def process_sample(idx: int, sample: dict) -> dict[str, Any]:
156
- nonlocal completed
157
-
158
- # Skip already-completed samples (resume)
159
- if idx in completed_by_idx:
160
- existing = completed_by_idx[idx]
161
- golden_field = task_config.get("golden_field")
162
- golden = sample.get(golden_field, "") if golden_field else ""
163
- return {**existing, "_golden_raw": golden}
164
-
165
- async with semaphore:
166
- input_data = {
167
- field: sample.get(field, "")
168
- for field in task_config.get("input_fields", [])
169
- }
170
-
171
- # Get golden reference
172
- golden_field = task_config.get("golden_field")
173
- golden_fields = task_config.get("golden_fields")
174
- if golden_field:
175
- golden = sample.get(golden_field, "")
176
- elif golden_fields:
177
- golden = {k: sample.get(v, "") for k, v in golden_fields.items()}
178
- else:
179
- golden = ""
180
-
181
- eval_sample = EvalSample(input=input_data, reference=golden)
182
-
183
- try:
184
- eval_sample = await pipeline.run(
185
- sample=eval_sample,
186
- prompts=prompt.get("system_prompt", ""),
187
- provider=self.inference_provider,
188
- model=prompt.get("model", "solar-pro2"),
189
- temperature=prompt.get("temperature", 0.0),
190
- max_tokens=prompt.get("max_tokens", 8000),
191
- reasoning_effort=prompt.get("reasoning_effort"),
192
- messages=prompt.get("messages"),
193
- )
194
- except Exception as e:
195
- ts = datetime.now().strftime("%H:%M:%S")
196
- logger.warning(f"[{ts}] Sample {idx} failed: {type(e).__name__}: {e}")
197
- raise
198
-
199
- # ์ €์žฅ ํ˜•ํƒœ๋Š” ์ด์ „๊ณผ ๊ฐ™์€ 8ํ‚ค dict (eval-store ์˜ resultRowSchema ๊ฐ€
200
- # ์ฝ๋Š” ์ด๋ฆ„๋“ค๊ณผ ํ•˜์œ„ ํ˜ธํ™˜) + trace ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. trace ๋Š” artifacts
201
- # ์ „์ฒด๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ๋Š”๋‹ค -- step_outputs ๋ฟ ์•„๋‹ˆ๋ผ v24 ๊ฐ€ ์ฑ„์šฐ๋Š”
202
- # judge_decisions/judge_tool_calls/self_consistency_runs/corrections
203
- # ๋„ ์—ฌ๊ธฐ ์•ˆ ๋„ฃ์œผ๋ฉด ์ €์žฅ ์ง์ „์— ํ†ต์งธ๋กœ ๋ฒ„๋ ค์ง„๋‹ค (์‹ค์ œ judge LLM ํ˜ธ์ถœยท
204
- # self-consistency ๋ฐ˜๋ณต ํ˜ธ์ถœ ๋น„์šฉ์ด ๋‚˜๊ฐ„ ์‚ฐ์ถœ๋ฌผ์ด๋‹ค). ํŠน์ • ํ‚ค๋งŒ
205
- # ํ•˜๋“œ์ฝ”๋”ฉํ•ด ์˜ฎ๊ธฐ๋ฉด ๋‹ค์Œ์— ํŒŒ์ดํ”„๋ผ์ธ์ด ์ƒˆ artifacts ํ‚ค๋ฅผ ์ถ”๊ฐ€ํ• 
206
- # ๋•Œ๋งˆ๋‹ค ์—ฌ๊ธฐ๋ฅผ ๋˜ ๊ณ ์ณ์•ผ ํ•˜๋ฏ€๋กœ ํ†ต์งธ๋กœ ๋„˜๊ธด๋‹ค --
207
- # resultRowSchema.trace ๋Š” .passthrough() ๋ผ ์—ฌ๋ถ„ ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ›๋Š”๋‹ค
208
- # (source-schemas.ts:223-228).
209
- run_result = {
210
- "run_id": run_id,
211
- "sample_idx": idx,
212
- "input": input_data,
213
- "output": eval_sample.output,
214
- "golden": golden,
215
- "input_tokens": eval_sample.artifacts.get("usage", {}).get(
216
- "prompt_tokens", 0
217
- ),
218
- "output_tokens": eval_sample.artifacts.get("usage", {}).get(
219
- "completion_tokens", 0
220
- ),
221
- "inference_time_ms": eval_sample.artifacts.get("inference_time_ms", 0),
222
- "trace": {
223
- "step_outputs": {},
224
- **eval_sample.artifacts,
225
- },
226
- }
227
- await self.store.insert_run_result(run_result)
228
-
229
- completed += 1
230
- ts = datetime.now().strftime("%H:%M:%S")
231
- logger.info(f"[{ts}] Sample {idx} completed ({completed}/{len(data)})")
232
- await self.store.update_run(run_id, {"completed_samples": completed})
233
- if on_progress:
234
- await on_progress(
235
- {
236
- "type": "inference_progress",
237
- "completed": completed,
238
- "total": len(data),
239
- "sample_idx": idx,
240
- }
241
- )
242
-
243
- return {**run_result, "_golden_raw": golden}
244
-
245
- tasks = [process_sample(i, sample) for i, sample in enumerate(data)]
246
- results = await asyncio.gather(*tasks, return_exceptions=True)
247
-
248
- # Filter out exceptions
249
- valid_results = [r for r in results if isinstance(r, dict)]
250
- errors = [(i, r) for i, r in enumerate(results) if isinstance(r, Exception)]
251
- if errors:
252
- logger.warning(f"Run {run_id}: {len(errors)}/{len(results)} samples failed")
253
- for sample_idx, err in errors:
254
- logger.warning(f" Sample {sample_idx} error: {type(err).__name__}: {err}")
255
-
256
- if on_progress:
257
- await on_progress({"type": "inference_complete", "total": len(valid_results)})
258
-
259
- # Run evaluation
260
- await self.store.update_run(run_id, {"status": RunStatus.EVALUATING})
261
- if on_progress:
262
- await on_progress({"type": "evaluation_start", "total": len(valid_results)})
263
-
264
- evaluator = create_evaluator(task_config.get("evaluator", {"type": "llm_judge"}))
265
- # eval_results: ์„ฑ๊ณตํ•œ evaluate() ๋ฐ˜ํ™˜๊ฐ’ + "sample_idx"(์ •๋ณธ, ยง0.5).
266
- # eval_failures: ์ฑ„์  ์ž์ฒด๊ฐ€ ์•ˆ ๋œ ์ƒ˜ํ”Œ -- aggregate() ์—๋Š” ์ ˆ๋Œ€ ์•ˆ ๋„˜๊ธด๋‹ค
267
- # (lcs_diff.aggregate() ์ฒ˜๋Ÿผ r["details"]["tp"] ๋ฅผ ์ง์ ‘ ์ฝ๋Š” ๊ตฌํ˜„์ด ์‹คํŒจ
268
- # ํ•ญ๋ชฉ์„ ๋งŒ๋‚˜๋ฉด KeyError ๋กœ ์ฃฝ๋Š”๋‹ค, ยง0.1).
269
- eval_results: list[dict[str, Any]] = []
270
- eval_failures: list[dict[str, Any]] = []
271
-
272
- for i, result in enumerate(valid_results):
273
- # F6: enumerate ์œ„์น˜๊ฐ€ ์•„๋‹ˆ๋ผ result["sample_idx"] ๊ฐ€ ์ •๋ณธ์ด๋‹ค --
274
- # ์ค‘๊ฐ„ ์ƒ˜ํ”Œ์ด ์ถ”๋ก ์—์„œ ์‹คํŒจํ•˜๋ฉด valid_results ์˜ ๋ฆฌ์ŠคํŠธ ์œ„์น˜์™€ ์›๋ณธ
275
- # sample_idx ๊ฐ€ ์–ด๊ธ‹๋‚œ๋‹ค.
276
- sample_idx = result["sample_idx"]
277
- try:
278
- eval_sample = build_eval_sample_from_result(
279
- result, reference=result["_golden_raw"]
280
- )
281
- evaluator.validate_required_fields(eval_sample)
282
- eval_result = await evaluator.evaluate(
283
- sample=eval_sample,
284
- provider=self.judge_provider,
285
- )
286
- eval_results.append({**eval_result, "sample_idx": sample_idx})
287
-
288
- if on_progress and i % 10 == 0:
289
- await on_progress(
290
- {
291
- "type": "evaluation_progress",
292
- "completed": i + 1,
293
- "total": len(valid_results),
294
- }
295
- )
296
- except Exception as e:
297
- logger.warning(f"Evaluation failed for sample {sample_idx}: {e}")
298
- eval_failures.append({"sample_idx": sample_idx, "error": str(e)})
299
-
300
- # Aggregate and save evaluation -- eval_results ์—๋Š” ์‹คํŒจ ํ•ญ๋ชฉ์ด ์•ˆ ์„ž์—ฌ
301
- # ์žˆ์œผ๋ฏ€๋กœ ๊ธฐ์กด aggregate() ๊ตฌํ˜„์ด ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค.
302
- aggregated = evaluator.aggregate(eval_results)
303
- # ์ฑ„์ ์— ์„ฑ๊ณตํ•œ ์ƒ˜ํ”Œ์ด ํ•˜๋‚˜๋„ ์—†์œผ๋ฉด ์ ์ˆ˜ ์ž๋ฆฌ๋ฅผ ๋น„์šด๋‹ค -- aggregate() ๋Š” ๋นˆ
304
- # ์ž…๋ ฅ์— 0.0 ์„ ๋Œ๋ ค์ฃผ๋Š”๋ฐ, ๊ทธ๊ฑด "0์ ์„ ๋ฐ›์•˜๋‹ค"๋Š” ์ธก์ •๊ฐ’์ด๋ผ "์ธก์ • ์ž์ฒด๊ฐ€
305
- # ์—†์—ˆ๋‹ค"์™€ ๊ตฌ๋ถ„๋˜์ง€ ์•Š๋Š”๋‹ค. judge ๊ฐ€ ํ†ต์งธ๋กœ ์ฃฝ์€ run ์ด evalhub ์ฐจํŠธ์—์„œ
306
- # ํ’ˆ์งˆ ๊ธ‰๋ฝ์œผ๋กœ ๋ณด์ด๋ฉด F1 ์„ ๋ฐ˜๋งŒ ๊ณ ์นœ ์…ˆ์ด๋‹ค.
307
- overall_score = aggregated.get("overall_score", 0.0) if eval_results else None
308
- eval_id = await self.store.create_evaluation(
309
- {
310
- "run_id": run_id,
311
- "scores": aggregated.get("scores", {}),
312
- "overall_score": overall_score,
313
- "eval_model": "gpt-4o",
314
- "eval_success_count": len(eval_results),
315
- "eval_failed_count": len(eval_failures),
316
- "failed_sample_indices": [f["sample_idx"] for f in eval_failures],
317
- }
318
- )
319
-
320
- # Save per-sample eval details -- ์„ฑ๊ณต/์‹คํŒจ ๋‘˜ ๋‹ค ํ•œ ํ–‰์”ฉ ๋‚จ๊ธด๋‹ค.
321
- # results.jsonl ๊ณผ eval_details.jsonl ์ด ํ•ญ์ƒ ๊ฐ™์€ sample_idx ์ง‘ํ•ฉ์„
322
- # ๊ฐ€๋ฆฌํ‚ค๊ฒŒ ํ•ด์„œ ๋ถ€๋ถ„์ ์œผ๋กœ๋งŒ ์ฑ„์ ๋œ run ์—์„œ "์ด ์ƒ˜ํ”Œ์€ ์™œ ์•ˆ ๋ณด์ด์ง€"๋ฅผ
323
- # ์—†์•ค๋‹ค.
324
- eval_detail_docs = []
325
- for er in eval_results:
326
- eval_detail_docs.append(
327
- {
328
- "evaluation_id": eval_id,
329
- "sample_idx": er["sample_idx"],
330
- "category_scores": er.get("category_scores", {}),
331
- "error_counts": {
332
- k: v.get("error_count", 0)
333
- for k, v in er.get("details", {}).items()
334
- if isinstance(v, dict)
335
- },
336
- "severity": er.get("severity", ""),
337
- "score": er.get("score", 0.0),
338
- }
339
  )
340
- for f in eval_failures:
341
- eval_detail_docs.append(
342
- {
343
- "evaluation_id": eval_id,
344
- "sample_idx": f["sample_idx"],
345
- "category_scores": {},
346
- "error_counts": {},
347
- "severity": None,
348
- # 0.0 ์ด ์•„๋‹ˆ๋ผ None -- ์ฑ„์  ์‹คํŒจ๋ฅผ ์ตœ์ € ์ ์ˆ˜์™€ ๊ตฌ๋ถ„ํ•œ๋‹ค (F1).
349
- "score": None,
350
- "error": f["error"],
351
- }
352
  )
353
- await self.store.insert_eval_details(eval_detail_docs)
354
-
355
- # Mark run as completed/partial -- ์ถ”๋ก ์ด ์ผ๋ถ€ ์ƒ˜ํ”Œ์—์„œ ์‹คํŒจํ–ˆ์œผ๋ฉด
356
- # PARTIAL, ํ‰๊ฐ€ ์‹คํŒจ๋Š” ์ด ์ƒํƒœ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค(ํ‰๊ฐ€ ์„ฑ๊ณต/์‹คํŒจ๋Š”
357
- # ์œ„ eval_success_count/eval_failed_count ๋กœ๋งŒ ํ‘œํ˜„ํ•œ๋‹ค, ยง0.4). errors
358
- # ์˜ ์ธ๋ฑ์Šค๋Š” asyncio.gather ๊ฐ€ tasks ์ˆœ์„œ๋ฅผ ๋ณด์กดํ•˜๋ฏ€๋กœ ์ด๋ฏธ ์ง„์งœ
359
- # sample_idx ๋‹ค.
360
- final_status = RunStatus.PARTIAL if errors else RunStatus.COMPLETED
361
- await self.store.update_run(
362
- run_id,
363
- {
364
- "status": final_status,
365
- "completed_at": datetime.now(timezone.utc),
366
- "failed_samples": len(errors),
367
- "failed_sample_indices": [i for i, _ in errors],
368
- },
369
  )
370
-
371
- if on_progress:
372
- await on_progress(
373
- {
374
- "type": "done",
375
- "overall_score": aggregated.get("overall_score", 0.0),
376
- "scores": aggregated.get("scores", {}),
377
- }
378
- )
379
-
380
  except Exception as e:
381
  logger.exception(f"Run {run_id} failed")
382
  await self.store.update_run(
@@ -389,6 +202,185 @@ class BatchRunner:
389
  if on_progress:
390
  await on_progress({"type": "error", "error": str(e)})
391
  # F4: ์ƒํƒœ๋ฅผ FAILED ๋กœ ๋‚จ๊ธฐ๊ณ  ์ง„ํ–‰ ์ƒํ™ฉ๊นŒ์ง€ ์•Œ๋ฆฐ ๋’ค **๊ทธ๋Œ€๋กœ ์žฌ์ „ํŒŒํ•œ๋‹ค**.
392
- # ์—ฌ๊ธฐ์„œ ์‚ผํ‚ค๋ฉด ํ˜ธ์ถœ์ž(CLI)๋Š” "Results saved to ..." ๋ฅผ ์ฐ๊ณ  evalhub ์ ์žฌ๊นŒ์ง€
393
  # ์‹œ๋„ํ•œ ๋‹ค์Œ exit 0 ์„ ๋‚ธ๋‹ค -- ๋ฐ์ดํ„ฐ์…‹์กฐ์ฐจ ๋ชป ์ฝ์€ run ์ด ์„ฑ๊ณต์œผ๋กœ ๋ณด์ธ๋‹ค.
394
  raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import asyncio
2
  import logging
3
  import sys
4
+ from dataclasses import dataclass
5
  from datetime import datetime, timezone
6
  from typing import Any, Callable
7
 
 
9
  logging.basicConfig(stream=sys.stderr, level=logging.INFO, format="%(message)s")
10
 
11
  from solar_eval.core.dataset_loader import DatasetLoader
12
+ from solar_eval.core.dataset_prep import (
13
+ PreparedDataset,
14
+ check_resume_indices,
15
+ prepare_dataset,
16
+ row_meta,
17
+ )
18
+ from solar_eval.core.pipeline_factory import build_task_pipeline
19
+ from solar_eval.core.row_filter import RowSelection
20
+ from solar_eval.core.run_records import (
21
+ FAILED_SAMPLE_ERRORS_KEY,
22
+ build_eval_detail_docs,
23
+ build_eval_sample_from_result,
24
+ build_evaluation_doc,
25
+ build_failure_errors,
26
+ build_result_row,
27
+ extract_golden,
28
+ extract_input,
29
+ )
30
  from solar_eval.evaluators.registry import create_evaluator
31
  from solar_eval.models.enums import RunStatus
32
  from solar_eval.models.sample import EvalSample
33
+ from solar_eval.pipelines.base import BasePipeline
34
  from solar_eval.providers.base import BaseProvider
35
  from solar_eval.stores.base import ResultStore
36
 
37
+ # `build_eval_sample_from_result` ๋Š” run_records ๋กœ ์˜ฎ๊ฒผ์ง€๋งŒ `runs eval` ๋“ฑ ๊ธฐ์กด ํ˜ธ์ถœ์ž๊ฐ€
38
+ # ์ด ๋ชจ๋“ˆ์—์„œ import ํ•˜๋ฏ€๋กœ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœํ•œ๋‹ค.
39
+ __all__ = ["BatchRunner", "build_eval_sample_from_result", "resolve_selection"]
40
+
41
  logger = logging.getLogger(__name__)
42
 
43
  OnProgress = Callable[[dict[str, Any]], Any]
44
 
45
+ # ํƒœ์Šคํฌ์— evaluator ์„ ์–ธ์ด ์—†์„ ๋•Œ ์“ฐ๋Š” ๊ธฐ๋ณธ ํ‰๊ฐ€๊ธฐ.
46
+ DEFAULT_EVALUATOR_CONFIG: dict[str, Any] = {"type": "llm_judge"}
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
+ def resolve_selection(
50
+ task_config: dict[str, Any],
51
+ selection: RowSelection | None,
52
+ prepared: PreparedDataset | None,
53
+ limit: int | None,
54
+ ) -> RowSelection | None:
55
+ """๋Ÿฌ๋„ˆ๊ฐ€ ๋”ฐ๋ฅผ ์„ ํƒ์„ ์ •ํ•œ๋‹ค -- ๋Ÿฌ๋„ˆ๋Š” ๋„˜๊ฒจ๋ฐ›์€ ์„ ํƒ๋งŒ ๋”ฐ๋ฅธ๋‹ค(๊ณ„ํš D3).
56
 
57
+ - `prepared` ๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ์„ ํƒ์ด ์ •๋ณธ์ด๋‹ค. `selection`ยท`limit` ๊ฐ€ ํ•จ๊ป˜ ์˜ค๋ฉด ๊ฐ™์•„์•ผ ํ•œ๋‹ค.
58
+ - ์˜› `limit` ์ธ์ž๋งŒ ์˜ค๋ฉด `RowSelection(limit=...)` ๋กœ ๋ฐ”๊พผ๋‹ค(0ยทNone = ์ „์ฒด, ์˜ค๋Š˜ ๋™์ž‘).
59
+ - ๋Ÿฌ๋„ˆ๋Š” `task_config["where"]` ๋ฅผ ์Šค์Šค๋กœ ์ฝ์ง€ ์•Š๋Š”๋‹ค. ํ˜ธ์ถœ์ž๊ฐ€ ์„ ํƒ์„ ๋„˜๊ธฐ์ง€ ์•Š์•˜๋Š”๋ฐ
60
+ ํƒœ์Šคํฌ์— where ๊ฐ€ ์žˆ์œผ๋ฉด ๊ฒฝ๊ณ ๋งŒ ๋‚จ๊ธฐ๊ณ  ์ „์ฒด ํ–‰์„ ๋ˆ๋‹ค(์„œ๋น„์Šค ๊ฒฝ๋กœ -- ์กฐ์šฉํ•œ ํ•„ํ„ฐ run ๊ธˆ์ง€).
61
  """
62
+ legacy_limit = limit or None
63
+ if prepared is not None:
64
+ if selection is not None and selection != prepared.selection:
65
+ raise ValueError("execute_run: selection differs from prepared.selection")
66
+ selection = prepared.selection
67
+ prepared_limit = selection.limit if selection is not None else None
68
+ if legacy_limit is not None and legacy_limit != prepared_limit:
69
+ raise ValueError("execute_run: limit differs from prepared.selection.limit")
70
+ elif selection is not None:
71
+ if legacy_limit is not None and legacy_limit != selection.limit:
72
+ raise ValueError("execute_run: limit differs from selection.limit")
73
+ elif legacy_limit is not None:
74
+ selection = RowSelection(limit=legacy_limit)
75
+
76
+ # CLI ๋Š” ํƒœ์Šคํฌ where ๋ฅผ ์„ ํƒ์— ํ•ฉ์ณ ๋„˜๊ธฐ๋ฏ€๋กœ, ํƒœ์Šคํฌ์— where ๊ฐ€ ์žˆ๋Š”๋ฐ ์ ์šฉํ•  ์„ ํƒ์—
77
+ # where ๊ฐ€ ์—†๋‹ค๋ฉด ํ˜ธ์ถœ์ž๊ฐ€ where ๋ฅผ ๋ชจ๋ฅด๋Š” ๊ฒฝ๋กœ(์„œ๋น„์Šคยท์˜› ํ˜ธ์ถœ์ž)๋‹ค.
78
+ if task_config.get("where") is not None and (selection is None or selection.where is None):
79
+ logger.warning(
80
+ "Task %r declares where, but where is not applied on this path -- running all rows",
81
+ task_config.get("name"),
82
+ )
83
+ return selection
84
+
85
+
86
+ def _index_completed(
87
+ completed_results: list[dict[str, Any]] | None, prepared: PreparedDataset
88
+ ) -> dict[int, dict[str, Any]]:
89
+ """resume ์šฉ `{sample_idx: ๊ฒฐ๊ณผ}`. ์„ ํƒ ๋ฐ– idx ๊ฐ€ ์„ž์—ฌ ์žˆ์œผ๋ฉด ์‹คํŒจํ•œ๋‹ค(D3)."""
90
+ completed_by_idx = {r["sample_idx"]: r for r in completed_results or []}
91
+ check_resume_indices(prepared, set(completed_by_idx))
92
+ return completed_by_idx
93
+
94
+
95
+ @dataclass
96
+ class _RunContext:
97
+ """ํ•œ run ์˜ ์‹คํ–‰ ์ƒํƒœ -- ์ƒ˜ํ”Œ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜๋“ค์ด ๊ณต์œ ํ•œ๋‹ค."""
98
+
99
+ run_id: str
100
+ task_config: dict[str, Any]
101
+ prompt: dict[str, Any]
102
+ pipeline: BasePipeline
103
+ prepared: PreparedDataset
104
+ completed_by_idx: dict[int, dict[str, Any]]
105
+ on_progress: OnProgress | None
106
+ semaphore: asyncio.Semaphore
107
+ completed: int = 0
108
+
109
+ @property
110
+ def total(self) -> int:
111
+ return self.prepared.rows_selected
112
 
113
 
114
  class BatchRunner:
 
136
  max_workers: int = 5,
137
  limit: int | None = None,
138
  completed_results: list[dict[str, Any]] | None = None,
139
+ selection: RowSelection | None = None,
140
+ prepared: PreparedDataset | None = None,
141
+ pipeline: BasePipeline | None = None,
142
  ) -> None:
143
  """Execute a full inference + evaluation run.
144
 
145
  Args:
146
  completed_results: Previously completed results for resume.
147
  Samples with matching sample_idx will be skipped.
148
+ selection: ๋Œ ํ–‰์˜ ์„ ํƒ(`where`ยท`limit`). `None` = ์ „์ฒด. ๋Ÿฌ๋„ˆ๋Š” ํƒœ์Šคํฌ์˜
149
+ `where` ๋ฅผ ์Šค์Šค๋กœ ์ฝ์ง€ ์•Š๋Š”๋‹ค -- ํ˜ธ์ถœ์ž(CLI)๊ฐ€ ๋งŒ๋“ค์–ด ๋„˜๊ธด๋‹ค(D3).
150
+ prepared: ํ˜ธ์ถœ์ž๊ฐ€ ๋ฏธ๋ฆฌ ์ฝ๊ณ  ๊ณ ๋ฅธ ๋ฐ์ดํ„ฐ์…‹. ์ฃผ๋ฉด ๋Ÿฌ๋„ˆ๊ฐ€ ๋‹ค์‹œ ์ฝ์ง€ ์•Š๋Š”๋‹ค --
151
+ run.json ์˜ sha256 ๊ณผ ์‹ค์ œ๋กœ ๋ˆ ํ–‰์ด ๊ฐ™์€ ๋ฐ”์ดํŠธ์—์„œ ๋‚˜์˜จ๋‹ค(D16).
152
+ pipeline: ํ˜ธ์ถœ์ž๊ฐ€ ๋งŒ๋“  ํŒŒ์ดํ”„๋ผ์ธ. ์Šค๋ƒ…์ƒท์„ ๋œฌ ๊ฒƒ๊ณผ **๊ฐ™์€ ์ธ์Šคํ„ด์Šค**๋ฅผ
153
+ ๋Œ๋ฆฌ๊ธฐ ์œ„ํ•ด ๋ฐ›๋Š”๋‹ค(D6). ์—†์œผ๋ฉด `build_task_pipeline` ์œผ๋กœ ๋งŒ๋“ ๋‹ค.
154
+
155
+ `sample_idx` ๋Š” ํ•„ํ„ฐยทlimit ๋’ค์—๋„ ์›๋ณธ ํŒŒ์ผ์˜ 0-based ๋ ˆ์ฝ”๋“œ ์œ„์น˜๋‹ค(D2).
156
 
157
  ์ƒ˜ํ”Œ ๋‹จ์œ„ ์‹คํŒจ(์ถ”๋ก /ํ‰๊ฐ€)๋Š” ์‚ผํ‚ค๊ณ  ์ƒํƒœ(PARTIAL/eval_failed_count)๋กœ ๊ธฐ๋กํ•˜์ง€๋งŒ,
158
+ run ์„ ํ†ต์งธ๋กœ ๋ชป ๋Œ๊ฒŒ ๋งŒ๋“œ๋Š” ์˜ˆ์™ธ(๋ฐ์ดํ„ฐ์…‹ ๋กœ๋“œ ์‹คํŒจ, 0ํ–‰ ์„ ํƒ, ์„ ํƒ ๋ฐ– resume,
159
+ ํŒŒ์ดํ”„๋ผ์ธ/ํ‰๊ฐ€๊ธฐ ์ƒ์„ฑ ์‹คํŒจ, ์Šคํ† ์–ด ์“ฐ๊ธฐ ์‹คํŒจ ๋“ฑ)๋Š” ์ƒํƒœ๋ฅผ FAILED ๋กœ ๋‚จ๊ธด ๋’ค
160
+ ๊ทธ๋Œ€๋กœ ์žฌ์ „ํŒŒํ•œ๋‹ค -- "๋ฌด์Šจ ์ผ์ด ์žˆ์–ด๋„ ์˜ˆ์™ธ ์—†์ด ๋ฐ˜ํ™˜"์ด ์•„๋‹ˆ๋‹ค. ํ˜ธ์ถœ์ž๋Š” ์ด
161
+ ํ•จ์ˆ˜๊ฐ€ raise ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด์•ผ ํ•œ๋‹ค.
162
  """
163
  try:
 
 
 
 
 
 
 
164
  await self.store.update_run(
165
  run_id,
166
  {
 
168
  "started_at": datetime.now(timezone.utc),
169
  },
170
  )
171
+ effective = resolve_selection(task_config, selection, prepared, limit)
172
+ if prepared is None:
173
+ prepared = prepare_dataset(
174
+ self.dataset_loader, project_config, task_config, effective
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  )
176
+ completed_by_idx = _index_completed(completed_results, prepared)
177
+ if pipeline is None:
178
+ pipeline = build_task_pipeline(
179
+ project_config, task_config, prompt, self.dataset_loader
 
 
 
 
 
 
 
 
180
  )
181
+ ctx = _RunContext(
182
+ run_id=run_id,
183
+ task_config=task_config,
184
+ prompt=prompt,
185
+ pipeline=pipeline,
186
+ prepared=prepared,
187
+ completed_by_idx=completed_by_idx,
188
+ on_progress=on_progress,
189
+ completed=len(completed_by_idx),
190
+ semaphore=asyncio.Semaphore(max_workers),
 
 
 
 
 
 
191
  )
192
+ await self._run_all(ctx)
 
 
 
 
 
 
 
 
 
193
  except Exception as e:
194
  logger.exception(f"Run {run_id} failed")
195
  await self.store.update_run(
 
202
  if on_progress:
203
  await on_progress({"type": "error", "error": str(e)})
204
  # F4: ์ƒํƒœ๋ฅผ FAILED ๋กœ ๋‚จ๊ธฐ๊ณ  ์ง„ํ–‰ ์ƒํ™ฉ๊นŒ์ง€ ์•Œ๋ฆฐ ๋’ค **๊ทธ๋Œ€๋กœ ์žฌ์ „ํŒŒํ•œ๋‹ค**.
205
+ # ์—ฌ๊ธฐ์„œ ์‚ผํ‚ค๋ฉด ํ˜ธ์ถœ์ž(CLI)๋Š” "Results saved to ..." ๋ฅผ ์ฐ๊ณ  Experiment Hub ์ ์žฌ๊นŒ์ง€
206
  # ์‹œ๋„ํ•œ ๋‹ค์Œ exit 0 ์„ ๋‚ธ๋‹ค -- ๋ฐ์ดํ„ฐ์…‹์กฐ์ฐจ ๋ชป ์ฝ์€ run ์ด ์„ฑ๊ณต์œผ๋กœ ๋ณด์ธ๋‹ค.
207
  raise
208
+
209
+ async def _run_all(self, ctx: _RunContext) -> None:
210
+ """์ถ”๋ก  โ†’ ์ฑ„์  โ†’ ์ƒํƒœ ๋งˆ๊ฐ. ์น˜๋ช…์  ์˜ˆ์™ธ๋Š” ํ˜ธ์ถœ์ž(`execute_run`)๊ฐ€ FAILED ๋กœ ๋‚จ๊ธด๋‹ค."""
211
+ await self.store.update_run(ctx.run_id, {"total_samples": ctx.total})
212
+ if ctx.on_progress:
213
+ await ctx.on_progress({"type": "dataset_loaded", "total": ctx.total})
214
+ if ctx.completed_by_idx:
215
+ remaining = ctx.total - len(ctx.completed_by_idx)
216
+ logger.info(f"Resuming: {len(ctx.completed_by_idx)} done, {remaining} remaining")
217
+
218
+ valid_results, errors = await self._infer_all(ctx)
219
+ aggregated = await self._evaluate_all(ctx, valid_results)
220
+
221
+ # Mark run as completed/partial -- ์ถ”๋ก ์ด ์ผ๋ถ€ ์ƒ˜ํ”Œ์—์„œ ์‹คํŒจํ–ˆ์œผ๋ฉด PARTIAL, ํ‰๊ฐ€
222
+ # ์‹คํŒจ๋Š” ์ด ์ƒํƒœ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค(ํ‰๊ฐ€ ์„ฑ๊ณต/์‹คํŒจ๋Š” eval_success_count/
223
+ # eval_failed_count ๋กœ๋งŒ ํ‘œํ˜„ํ•œ๋‹ค, ยง0.4). errors ์˜ ์ธ๋ฑ์Šค๋Š” `_infer_all` ์ด
224
+ # gather ์œ„์น˜๋ฅผ ์›๋ณธ sample_idx ๋กœ ๋งคํ•‘ํ•ด ๋‘” ๊ฐ’์ด๋‹ค(D2).
225
+ final_status = RunStatus.PARTIAL if errors else RunStatus.COMPLETED
226
+ await self.store.update_run(
227
+ ctx.run_id,
228
+ {
229
+ "status": final_status,
230
+ "completed_at": datetime.now(timezone.utc),
231
+ "failed_samples": len(errors),
232
+ "failed_sample_indices": [i for i, _ in errors],
233
+ # ๊ฐ™์€ ํ˜ธ์ถœ์—์„œ ์“ด๋‹ค -- ๋‘ ๋ชฉ๋ก์˜ ์œ„์น˜ ์ง‘ํ•ฉ์ด ๋Š˜ ๊ฐ™๋‹ค. ํƒ€์ž… ์ด๋ฆ„๋งŒ(LP5 D-LP5-4).
234
+ FAILED_SAMPLE_ERRORS_KEY: build_failure_errors(errors),
235
+ },
236
+ )
237
+ if ctx.on_progress:
238
+ await ctx.on_progress(
239
+ {
240
+ "type": "done",
241
+ "overall_score": aggregated.get("overall_score", 0.0),
242
+ "scores": aggregated.get("scores", {}),
243
+ }
244
+ )
245
+
246
+ async def _infer_all(
247
+ self, ctx: _RunContext
248
+ ) -> tuple[list[dict[str, Any]], list[tuple[int, BaseException]]]:
249
+ """์„ ํƒ๋œ ํ–‰์„ ๋™์‹œ์— ์ถ”๋ก ํ•œ๋‹ค. ์‹คํŒจ๋Š” `(์›๋ณธ sample_idx, ์˜ˆ์™ธ)` ๋กœ ๋Œ๋ ค์ค€๋‹ค."""
250
+ selected = ctx.prepared.selected
251
+ tasks = [self._infer_one(ctx, idx, sample) for idx, sample in selected]
252
+ results = await asyncio.gather(*tasks, return_exceptions=True)
253
+
254
+ valid_results = [r for r in results if isinstance(r, dict)]
255
+ # gather ๋Š” tasks ์ˆœ์„œ๋ฅผ ๋ณด์กดํ•˜๋ฏ€๋กœ ๊ฒฐ๊ณผ ์œ„์น˜ = selected ์œ„์น˜๋‹ค. ์œ„์น˜๋ฅผ ๊ทธ๋Œ€๋กœ
256
+ # ์“ฐ๋ฉด ํ•„ํ„ฐยทlimit run ์—์„œ ์—‰๋šฑํ•œ ํ–‰์„ ์‹คํŒจ๋กœ ์ ๋Š”๋‹ค -- ์›๋ณธ idx ๋กœ ๋งคํ•‘ํ•œ๋‹ค(D2).
257
+ errors = [
258
+ (selected[pos][0], r) for pos, r in enumerate(results) if isinstance(r, BaseException)
259
+ ]
260
+ if errors:
261
+ logger.warning(f"Run {ctx.run_id}: {len(errors)}/{len(results)} samples failed")
262
+ for sample_idx, err in errors:
263
+ logger.warning(f" Sample {sample_idx} error: {type(err).__name__}: {err}")
264
+
265
+ if ctx.on_progress:
266
+ await ctx.on_progress({"type": "inference_complete", "total": len(valid_results)})
267
+ return valid_results, errors
268
+
269
+ async def _infer_one(self, ctx: _RunContext, idx: int, sample: dict) -> dict[str, Any]:
270
+ """์ƒ˜ํ”Œ ํ•˜๋‚˜ ์ถ”๋ก ยท์ €์žฅ. `idx` ๋Š” ์›๋ณธ ํŒŒ์ผ ์œ„์น˜๋‹ค."""
271
+ golden = extract_golden(sample, ctx.task_config)
272
+ # Skip already-completed samples (resume)
273
+ if idx in ctx.completed_by_idx:
274
+ return {**ctx.completed_by_idx[idx], "_golden_raw": golden}
275
+
276
+ async with ctx.semaphore:
277
+ input_data = extract_input(sample, ctx.task_config.get("input_fields", []))
278
+ eval_sample = EvalSample(input=input_data, reference=golden)
279
+ prompt = ctx.prompt
280
+ try:
281
+ eval_sample = await ctx.pipeline.run(
282
+ sample=eval_sample,
283
+ prompts=prompt.get("system_prompt", ""),
284
+ provider=self.inference_provider,
285
+ model=prompt.get("model", "solar-pro2"),
286
+ temperature=prompt.get("temperature", 0.0),
287
+ max_tokens=prompt.get("max_tokens", 8000),
288
+ reasoning_effort=prompt.get("reasoning_effort"),
289
+ messages=prompt.get("messages"),
290
+ )
291
+ except Exception as e:
292
+ ts = datetime.now().strftime("%H:%M:%S")
293
+ logger.warning(f"[{ts}] Sample {idx} failed: {type(e).__name__}: {e}")
294
+ raise
295
+
296
+ run_result = build_result_row(
297
+ ctx.run_id,
298
+ idx,
299
+ input_data,
300
+ golden,
301
+ eval_sample,
302
+ meta=row_meta(sample, ctx.prepared.meta_fields),
303
+ )
304
+ await self.store.insert_run_result(run_result)
305
+ await self._report_inference(ctx, idx)
306
+ return {**run_result, "_golden_raw": golden}
307
+
308
+ async def _report_inference(self, ctx: _RunContext, idx: int) -> None:
309
+ ctx.completed += 1
310
+ ts = datetime.now().strftime("%H:%M:%S")
311
+ logger.info(f"[{ts}] Sample {idx} completed ({ctx.completed}/{ctx.total})")
312
+ await self.store.update_run(ctx.run_id, {"completed_samples": ctx.completed})
313
+ if ctx.on_progress:
314
+ await ctx.on_progress(
315
+ {
316
+ "type": "inference_progress",
317
+ "completed": ctx.completed,
318
+ "total": ctx.total,
319
+ "sample_idx": idx,
320
+ }
321
+ )
322
+
323
+ async def _evaluate_all(
324
+ self, ctx: _RunContext, valid_results: list[dict[str, Any]]
325
+ ) -> dict[str, Any]:
326
+ """์ฑ„์ ํ•˜๊ณ  evaluationยทeval_details ๋ฅผ ์ €์žฅํ•œ๋‹ค. ์ง‘๊ณ„ dict ๋ฅผ ๋Œ๋ ค์ค€๋‹ค."""
327
+ await self.store.update_run(ctx.run_id, {"status": RunStatus.EVALUATING})
328
+ if ctx.on_progress:
329
+ await ctx.on_progress({"type": "evaluation_start", "total": len(valid_results)})
330
+
331
+ evaluator = create_evaluator(ctx.task_config.get("evaluator", DEFAULT_EVALUATOR_CONFIG))
332
+ eval_results, eval_failures = await self._score(ctx, evaluator, valid_results)
333
+
334
+ # eval_results ์—๋Š” ์‹คํŒจ ํ•ญ๋ชฉ์ด ์•ˆ ์„ž์—ฌ ์žˆ์œผ๋ฏ€๋กœ ๊ธฐ์กด aggregate() ๊ตฌํ˜„์ด ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค.
335
+ aggregated = evaluator.aggregate(eval_results)
336
+ eval_id = await self.store.create_evaluation(
337
+ build_evaluation_doc(
338
+ # eval_model = ํ‰๊ฐ€๊ธฐ๊ฐ€ ๋ณด๊ณ ํ•˜๋Š” judge ๋ชจ๋ธ(์—†์œผ๋ฉด None) -- ์˜ˆ์ „์˜ ํ•˜๋“œ์ฝ”๋”ฉ
339
+ # "gpt-4o" ๋Š” lcs_diff run ์—๋„ judge ๊ฐ€ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ์ ์—ˆ๋‹ค(๊ณ„ํš D11).
340
+ ctx.run_id,
341
+ aggregated,
342
+ eval_results,
343
+ eval_failures,
344
+ evaluator.judge_model,
345
+ )
346
+ )
347
+ await self.store.insert_eval_details(
348
+ build_eval_detail_docs(eval_id, eval_results, eval_failures)
349
+ )
350
+ return aggregated
351
+
352
+ async def _score(
353
+ self, ctx: _RunContext, evaluator: Any, valid_results: list[dict[str, Any]]
354
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
355
+ """์ƒ˜ํ”Œ๋ณ„ ์ฑ„์ .
356
+
357
+ eval_results: ์„ฑ๊ณตํ•œ evaluate() ๋ฐ˜ํ™˜๊ฐ’ + "sample_idx"(์ •๋ณธ, ยง0.5).
358
+ eval_failures: ์ฑ„์  ์ž์ฒด๊ฐ€ ์•ˆ ๋œ ์ƒ˜ํ”Œ -- aggregate() ์—๋Š” ์ ˆ๋Œ€ ์•ˆ ๋„˜๊ธด๋‹ค
359
+ (lcs_diff.aggregate() ์ฒ˜๋Ÿผ r["details"]["tp"] ๋ฅผ ์ง์ ‘ ์ฝ๋Š” ๊ตฌํ˜„์ด ์‹คํŒจ ํ•ญ๋ชฉ์„
360
+ ๋งŒ๋‚˜๋ฉด KeyError ๋กœ ์ฃฝ๋Š”๋‹ค, ยง0.1).
361
+ """
362
+ eval_results: list[dict[str, Any]] = []
363
+ eval_failures: list[dict[str, Any]] = []
364
+ for i, result in enumerate(valid_results):
365
+ # F6: enumerate ์œ„์น˜๊ฐ€ ์•„๋‹ˆ๋ผ result["sample_idx"] ๊ฐ€ ์ •๋ณธ์ด๋‹ค.
366
+ sample_idx = result["sample_idx"]
367
+ try:
368
+ eval_sample = build_eval_sample_from_result(result, reference=result["_golden_raw"])
369
+ evaluator.validate_required_fields(eval_sample)
370
+ eval_result = await evaluator.evaluate(
371
+ sample=eval_sample,
372
+ provider=self.judge_provider,
373
+ )
374
+ eval_results.append({**eval_result, "sample_idx": sample_idx})
375
+ if ctx.on_progress and i % 10 == 0:
376
+ await ctx.on_progress(
377
+ {
378
+ "type": "evaluation_progress",
379
+ "completed": i + 1,
380
+ "total": len(valid_results),
381
+ }
382
+ )
383
+ except Exception as e:
384
+ logger.warning(f"Evaluation failed for sample {sample_idx}: {e}")
385
+ eval_failures.append({"sample_idx": sample_idx, "error": str(e)})
386
+ return eval_results, eval_failures
solar_eval/core/snapshot/__init__.py ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """run.json `snapshot` ๋ธ”๋ก(engine-snapshot/v1) โ€” "๋ฌด์—‡์œผ๋กœ ๋Œ๋ ธ๋Š”๊ฐ€"๋ฅผ ์™„์ „ํžˆ ์ ๋Š”๋‹ค.
2
+
3
+ ๊ณ„ํš 2026-09-24-phase1a D5~D12. ๋ชจ์–‘์˜ ์ •๋ณธ์€ poc-bundle(SPEC ยง3.4 ์™„์ „์„ฑ ๊ทœ๋ฒ” ํ‚ค 8๊ฐœ)์ด๊ณ , ์ง€๋ฌธ์€
4
+ `poc_bundle.snapshot.config_fingerprint`(JCS) ๋ฅผ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค โ€” ๊ทœ์น™์„ ๋‘ ๋ฒŒ ์งœ์ง€ ์•Š๋Š”๋‹ค.
5
+
6
+ - `config` = ์ง€๋ฌธ ๋Œ€์ƒ(๊ธฐ๋Šฅ). `provenance` = ์ง€๋ฌธ ๋ฐ–(์ด๋ฆ„ยท์œ„์น˜ยท์ปค๋ฐ‹ยท์‹คํ–‰ ์˜ต์…˜).
7
+ - `build_engine_snapshot(inputs)` ๋Š” run ์‹œ์ž‘ **์ „์—** ๋ถ€๋ฅธ๋‹ค. ์„ ์–ธํ–ˆ๋Š”๋ฐ ์—†๋Š” ์ž์›์€ ์—ฌ๊ธฐ์„œ ์ฃฝ๋Š”๋‹ค.
8
+ - `self_check(snapshot)` = ๊ทœ๋ฒ” ํ‚ค ๋ˆ„๋ฝ ์—†์Œ + ์ง€๋ฌธ ์žฌ๊ณ„์‚ฐ ์ผ์น˜ (์—”์ง„ ์ž๊ธฐ ๊ฒ€์ฆ, D5).
9
+
10
+ `poc_bundle`ยท`packaging` import ๋Š” ์ด ํŒจํ‚ค์ง€(์™€ cli ์˜ run ๋ฉ”ํƒ€ ๋ชจ๋“ˆ) ์•ˆ์—์„œ๋งŒ ํ•œ๋‹ค โ€” HF Space ๋ฒค๋”
11
+ ์‚ฌ๋ณธ์ด import ํ•˜๋Š” ๋ชจ๋“ˆ์€ ๋‘˜ ์—†์ด ๋Œ์•„์•ผ ํ•œ๋‹ค(D18).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import copy
17
+ import subprocess
18
+ from collections.abc import Mapping, Sequence
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from poc_bundle.canonical import json_fingerprint
24
+ from poc_bundle.snapshot import completeness_gaps, config_fingerprint
25
+
26
+ from solar_eval.core.snapshot import environment, impl_hash, reproducibility, resources
27
+ from solar_eval.core.snapshot.impl_hash import ImplRoots
28
+ from solar_eval.evaluators.registry import DEFAULT_EVALUATOR_CONFIG, create_evaluator
29
+
30
+ ENGINE_SNAPSHOT_SCHEMA = "engine-snapshot/v1"
31
+
32
+ #: `snapshot.config` ํ‚ค ์ˆœ์„œ (๊ณ„ํš ยง3.2). ์ง€๋ฌธ์€ JCS ๋ผ ์ˆœ์„œ์™€ ๋ฌด๊ด€ํ•˜์ง€๋งŒ ์‚ฌ๋žŒ์ด ์ฝ๋Š” ์ˆœ์„œ๋ฅผ ๊ณ ์ •ํ•œ๋‹ค.
33
+ CONFIG_KEYS = (
34
+ "model",
35
+ "temperature",
36
+ "max_tokens",
37
+ "reasoning_effort",
38
+ "pipeline_type",
39
+ "input_fields",
40
+ "resolved_pipeline",
41
+ "prompt_assets",
42
+ "resources",
43
+ "evaluator_config",
44
+ "step_impl_hash",
45
+ "runtime_impl_hash",
46
+ "dependency_lock",
47
+ "runner_version",
48
+ )
49
+
50
+ #: ๋Ÿฐํƒ€์ž„ยทํ‰๊ฐ€๊ธฐ ํํฌ ๋ชฉ๋ก์„ `provenance.impl_modules` ์— ๋‚จ๊ธธ ๋•Œ์˜ ํ‚ค.
51
+ RUNTIME_MODULES_KEY = "@runtime"
52
+ EVALUATOR_MODULES_KEY = "@evaluator"
53
+
54
+ #: ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ ์ด๋ฆ„์„ ๋‚จ๊ธฐ๋Š” provenance ํ‚ค โ€” ์žฌ์ฑ„์ ์ด ๋Ÿฐํƒ€์ž„ ํ•ด์‹œ๋ฅผ ๋‹ค์‹œ ๋œฐ ์žฌ๋ฃŒ.
55
+ PROVIDERS_KEY = "providers"
56
+
57
+ #: ์—”์ง„ ๋ ˆํฌ๋ฅผ ์ž๋™์œผ๋กœ ์ฐพ์œผ๋ผ๋Š” ํ‘œ์‹œ(`EngineSnapshotInputs.repo` ๊ธฐ๋ณธ๊ฐ’).
58
+ AUTO = "auto"
59
+
60
+
61
+ class SnapshotError(ValueError):
62
+ """์Šค๋ƒ…์ƒท์ด ์Šค์Šค๋กœ์˜ ๊ทœ์น™์„ ์–ด๊ฒผ๋‹ค โ€” ๊ทœ๋ฒ” ํ‚ค ๋ˆ„๋ฝยท์ง€๋ฌธ ๋ถˆ์ผ์น˜ยท๋ชจ๋ฅด๋Š” schema."""
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class EngineSnapshotInputs:
67
+ """์Šค๋ƒ…์ƒท ์žฌ๋ฃŒ. ํŒŒ์ผยทgit ์ฝ๊ธฐ๋Š” `roots`ยท`engine`ยท`repo`ยท`git_run` ์œผ๋กœ ๋ฐ”๊ฟ” ๋ผ์šธ ์ˆ˜ ์žˆ๋‹ค.
68
+
69
+ Attributes:
70
+ pipeline: ๋Ÿฌ๋„ˆ๊ฐ€ **์‹ค์ œ๋กœ ๋Œ๋ฆด** `Pipeline` ์ธ์Šคํ„ด์Šค(D6) โ€” `step_impl_hash`ยท`resources` ์žฌ๋ฃŒ.
71
+ resolved_pipeline: `extends`ยท`--set` ๊นŒ์ง€ ํ•ด์„๋œ ํŒŒ์ดํ”„๋ผ์ธ dict(์—†์œผ๋ฉด None/{}).
72
+ prompt_path: ํ”„๋กฌํ”„ํŠธ ๋””๋ ‰ํ† ๋ฆฌ ๋˜๋Š” ๋‹จ์ผ YAML ํŒŒ์ผ.
73
+ evaluator_config: ํƒœ์Šคํฌ์˜ evaluator ์„ค์ •. None ์€ "ํ‚ค ์—†์Œ"์œผ๋กœ ์ฝ์–ด ๋Ÿฌ๋„ˆ ๊ธฐ๋ณธ๊ฐ’
74
+ (`llm_judge`)์„ ์“ด๋‹ค. ํ‚ค๊ฐ€ ์žˆ๋Š”๋ฐ ๋น„์—ˆ์œผ๋ฉด(`{}`) ์—ฌ๊ธฐ์„œ ValueError โ€” ๋Ÿฌ๋„ˆ๊ฐ€ ์ฃฝ์„ ์„ค์ •์ด๋‹ค.
75
+ dataset: `dataset_block.describe_dataset` ๊ฒฐ๊ณผ(์žฌํ˜„ ํŒ์ • ์žฌ๋ฃŒ). None = ๋ชจ๋ฆ„.
76
+ engine: ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•œ `environment.engine_state` ๊ฒฐ๊ณผ. None ์ด๋ฉด ์—ฌ๊ธฐ์„œ ๊ณ„์‚ฐํ•œ๋‹ค.
77
+ repo: ์—”์ง„ ๋ ˆํฌ(uv.lockยทengine_state ์šฉ). `AUTO` ๋ฉด `environment.engine_repo()`.
78
+ """
79
+
80
+ model: str
81
+ temperature: float
82
+ max_tokens: int
83
+ reasoning_effort: str | None
84
+ pipeline_type: str
85
+ input_fields: Sequence[str]
86
+ resolved_pipeline: Mapping[str, Any] | None
87
+ prompt_path: Path
88
+ pipeline: Any
89
+ evaluator_config: Mapping[str, Any] | None
90
+ inference_provider: Any
91
+ judge_provider: Any = None
92
+ prompt_name: str | None = None
93
+ pipeline_name: str | None = None
94
+ config_source: str | None = None
95
+ git_commit: str | None = None
96
+ pipeline_overrides: Mapping[str, Any] | None = None
97
+ max_workers: int | None = None
98
+ config_dir: Path | None = None
99
+ repo_managed: bool = False
100
+ dataset: Mapping[str, Any] | None = None
101
+ roots: ImplRoots | None = None
102
+ engine: Mapping[str, Any] | None = None
103
+ repo: Path | str | None = AUTO
104
+ git_run: Any = field(default=subprocess.run)
105
+
106
+
107
+ def evaluator_block(
108
+ evaluator_config: Mapping[str, Any] | None, roots: ImplRoots | None = None
109
+ ) -> tuple[dict[str, Any], list[str]]:
110
+ """`config.evaluator_config = {config, judge_model, impl_hash}` ์™€ ํํฌ ๋ผ๋ฒจ.
111
+
112
+ ๊ธฐ๋ณธ๊ฐ’์€ `evaluator_config is None`(ํƒœ์Šคํฌ์— ํ‚ค๊ฐ€ ์—†์Œ)์ผ ๋•Œ๋งŒ ์“ด๋‹ค โ€” ๋Ÿฌ๋„ˆ์˜
113
+ `task_config.get("evaluator", DEFAULT)` ์™€ ๊ฐ™์€ ๊ทœ์น™. `{}`ยท`type` ์—†๋Š” ์„ค์ •์€ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ฎ์ง€ ์•Š๊ณ 
114
+ `create_evaluator` ๊ฐ€ ValueError ๋กœ ๊ฑฐ๋ถ€ํ•œ๋‹ค(์ถ”๋ก  ์ „์— ์ฃฝ๋Š”๋‹ค, D5).
115
+
116
+ Raises:
117
+ ValueError: ํ‰๊ฐ€๊ธฐ ์„ค์ •์ด ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๊ฑฐ๋‚˜ `type` ์ด ์—†๊ฑฐ๏ฟฝ๏ฟฝ๏ฟฝ ๋ชจ๋ฅด๋Š” ๊ฐ’์ผ ๋•Œ.
118
+ """
119
+ raw = DEFAULT_EVALUATOR_CONFIG if evaluator_config is None else evaluator_config
120
+ evaluator = create_evaluator(copy.deepcopy(raw))
121
+ effective = copy.deepcopy(dict(raw))
122
+ digest, modules = impl_hash.evaluator_impl_hash(evaluator, roots)
123
+ block = {"config": effective, "judge_model": evaluator.judge_model, "impl_hash": digest}
124
+ return block, modules
125
+
126
+
127
+ def _resolve_repo(inputs: EngineSnapshotInputs) -> Path | None:
128
+ if inputs.repo == AUTO:
129
+ return environment.engine_repo()
130
+ return Path(inputs.repo) if inputs.repo is not None else None
131
+
132
+
133
+ def _engine(inputs: EngineSnapshotInputs, repo: Path | None, roots: ImplRoots) -> dict[str, Any]:
134
+ """`provenance.engine` โ€” ์ฃผ์ž…๊ฐ’์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ, ๋ ˆํฌ๊ฐ€ ์—†์œผ๋ฉด ํŠธ๋ฆฌ ํ•ด์‹œ๋งŒ(commitยทdirty null)."""
135
+ if inputs.engine is not None:
136
+ return copy.deepcopy(dict(inputs.engine))
137
+ if repo is None:
138
+ tree = impl_hash.engine_tree_sha256(roots)
139
+ return {"commit": None, "tree_sha256": tree, "dirty": None, "dirty_scope": []}
140
+ return environment.engine_state(
141
+ inputs.config_dir,
142
+ inputs.repo_managed,
143
+ repo=repo,
144
+ roots=roots,
145
+ run=inputs.git_run,
146
+ )
147
+
148
+
149
+ def _recorded_providers(inputs: EngineSnapshotInputs) -> dict[str, str | None]:
150
+ """`provenance.providers = {inference, judge}` โ€” ์ด๋ฒˆ run ์— ์ฃผ์ž…๋œ ํ”„๋กœ๋ฐ”์ด๋”์˜ ๋ชจ๋“ˆ ์ด๋ฆ„.
151
+
152
+ judge ๋Š” ์“ฐ๋“  ์•ˆ ์“ฐ๋“  ์ ๋Š”๋‹ค โ€” ์žฌ์ฑ„์ (`with_evaluator`)์ด judge ๋ฅผ ์ƒˆ๋กœ ์“ฐ๊ฒŒ ๋˜๋ฉด ์ด ์ด๋ฆ„์œผ๋กœ
153
+ ๋Ÿฐํƒ€์ž„ ํ•ด์‹œ๋ฅผ ๋‹ค์‹œ ๋œฌ๋‹ค.
154
+ """
155
+ judge = inputs.judge_provider
156
+ return {
157
+ "inference": impl_hash.provider_module(inputs.inference_provider),
158
+ "judge": None if judge is None else impl_hash.provider_module(judge),
159
+ }
160
+
161
+
162
+ def _runtime(
163
+ providers: Mapping[str, str | None], judge_model: str | None, roots: ImplRoots
164
+ ) -> tuple[str, list[str]]:
165
+ """๋Ÿฐํƒ€์ž„ ํ•ด์‹œ โ€” ์ถ”๋ก  ํ”„๋กœ๋ฐ”์ด๋” + (ํ‰๊ฐ€๊ธฐ๊ฐ€ judge ๋ฅผ ์‹ค์ œ๋กœ ๋ถ€๋ฅผ ๋•Œ๋งŒ) judge ํ”„๋กœ๋ฐ”์ด๋”.
166
+
167
+ lcs_diff run ์€ judge ๋ฅผ ์•ˆ ์“ฐ๋ฏ€๋กœ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค. ๋นŒ๋“œ์™€ ์žฌ์ฑ„์ ์ด ๊ฐ™์€ ๊ทœ์น™์„ ์“ด๋‹ค.
168
+ """
169
+ used = [providers["inference"]]
170
+ if judge_model is not None and providers.get("judge") is not None:
171
+ used.append(providers["judge"])
172
+ return impl_hash.runtime_impl_hash_for_modules([m for m in used if m is not None], roots)
173
+
174
+
175
+ @dataclass(frozen=True)
176
+ class _Materials:
177
+ """`config` ์™€, ์ง€๋ฌธ ๋ฐ–์œผ๋กœ ๊ฐ€๋Š” ๋ถ€์‚ฐ๋ฌผ(์ž์› ํ•ด์„ยทํํฌ ๋ผ๋ฒจยทํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ)."""
178
+
179
+ config: dict[str, Any]
180
+ collected: resources.CollectedResources
181
+ impl_modules: dict[str, list[str]]
182
+ providers: dict[str, str | None]
183
+
184
+
185
+ def _collect_config(inputs: EngineSnapshotInputs, roots: ImplRoots) -> _Materials:
186
+ """์ง€๋ฌธ ๋Œ€์ƒ `config` ๋ฅผ `CONFIG_KEYS` ์ˆœ์„œ๋กœ ๋ชจ์€๋‹ค โ€” ํŒŒ์ผยท์ž์›ยท๊ตฌํ˜„ ํ•ด์‹œ๋ฅผ ์—ฌ๊ธฐ์„œ ์ฝ๋Š”๋‹ค."""
187
+ collected = resources.collect_resources(inputs.pipeline)
188
+ evaluator, evaluator_modules = evaluator_block(inputs.evaluator_config, roots)
189
+ step_hashes, step_modules = impl_hash.step_impl_hashes(inputs.pipeline, roots)
190
+ providers = _recorded_providers(inputs)
191
+ runtime_hash, runtime_modules = _runtime(providers, evaluator["judge_model"], roots)
192
+ config = {
193
+ "model": inputs.model,
194
+ "temperature": inputs.temperature,
195
+ "max_tokens": inputs.max_tokens,
196
+ "reasoning_effort": inputs.reasoning_effort,
197
+ "pipeline_type": inputs.pipeline_type,
198
+ "input_fields": list(inputs.input_fields),
199
+ "resolved_pipeline": copy.deepcopy(dict(inputs.resolved_pipeline or {})),
200
+ "prompt_assets": resources.prompt_assets(inputs.prompt_path),
201
+ "resources": collected.hashes,
202
+ "evaluator_config": evaluator,
203
+ "step_impl_hash": step_hashes,
204
+ "runtime_impl_hash": runtime_hash,
205
+ "dependency_lock": environment.dependency_lock(),
206
+ "runner_version": environment.runner_version(),
207
+ }
208
+ impl_modules = {
209
+ **step_modules,
210
+ EVALUATOR_MODULES_KEY: evaluator_modules,
211
+ RUNTIME_MODULES_KEY: runtime_modules,
212
+ }
213
+ config = {key: config[key] for key in CONFIG_KEYS}
214
+ return _Materials(config, collected, impl_modules, providers)
215
+
216
+
217
+ def _provenance(
218
+ inputs: EngineSnapshotInputs,
219
+ repo: Path | None,
220
+ engine: dict[str, Any],
221
+ materials: _Materials,
222
+ steps_use_model: bool,
223
+ ) -> dict[str, Any]:
224
+ """์ง€๋ฌธ ๋ฐ– ์ถœ์ฒ˜ โ€” ์ด๋ฆ„ยท์œ„์น˜ยท์ปค๋ฐ‹ยท์—”์ง„ ์ƒํƒœยท์„ค์น˜ ํํฌยท์‹คํ–‰ ์˜ต์…˜ (๊ณ„ํš ยง3.2 ์ˆœ์„œ)."""
225
+ return {
226
+ "prompt": inputs.prompt_name,
227
+ "pipeline": inputs.pipeline_name,
228
+ "config_source": inputs.config_source,
229
+ "git_commit": inputs.git_commit,
230
+ "pipeline_overrides": copy.deepcopy(inputs.pipeline_overrides),
231
+ "engine": engine,
232
+ "uv_lock_sha256": environment.uv_lock_sha256(repo),
233
+ "installed_closure": environment.installed_closure(),
234
+ "impl_modules": materials.impl_modules,
235
+ PROVIDERS_KEY: materials.providers,
236
+ "steps_use_model": steps_use_model,
237
+ "execution": {"max_workers": inputs.max_workers},
238
+ "resumes": [],
239
+ }
240
+
241
+
242
+ def _reproducibility(
243
+ inputs: EngineSnapshotInputs,
244
+ materials: _Materials,
245
+ engine: dict[str, Any],
246
+ steps_use_model: bool,
247
+ ) -> dict[str, Any]:
248
+ verdict, blockers = reproducibility.rerunnable(
249
+ engine_dirty=engine.get("dirty"),
250
+ repo_managed=inputs.repo_managed,
251
+ dataset=inputs.dataset,
252
+ resources=materials.collected.refs,
253
+ )
254
+ config = materials.config
255
+ return {
256
+ "engine_dirty": engine.get("dirty"),
257
+ "rerunnable": verdict,
258
+ "rerunnable_blockers": blockers,
259
+ "deterministic": reproducibility.deterministic(
260
+ temperature=inputs.temperature,
261
+ resolved_pipeline=config["resolved_pipeline"],
262
+ steps_use_model=steps_use_model,
263
+ judge_model=config["evaluator_config"]["judge_model"],
264
+ ),
265
+ }
266
+
267
+
268
+ def build_engine_snapshot(inputs: EngineSnapshotInputs) -> dict[str, Any]:
269
+ """`{schema, config, config_fingerprint, missing, provenance, reproducibility}` ๋ฅผ ์กฐ๋ฆฝํ•œ๋‹ค.
270
+
271
+ Raises:
272
+ KnowledgeAssetError: ์Šคํ…์ด ์„ ์–ธํ•œ ์ž์›์„ ๋กœ์ปฌ์—์„œ ์ฐพ์„ ์ˆ˜ ์—†์„ ๋•Œ(run ์‹œ์ž‘ ์ „).
273
+ FileNotFoundError / ValueError: ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋น„์—ˆ์„ ๋•Œ, ๊ตฌํ˜„ ๋ชจ๋“ˆ์„ ํ’€ ์ˆ˜ ์—†์„ ๋•Œ.
274
+ """
275
+ roots = inputs.roots or impl_hash.default_roots()
276
+ materials = _collect_config(inputs, roots)
277
+ repo = _resolve_repo(inputs)
278
+ engine = _engine(inputs, repo, roots)
279
+ steps_use_model = any(step.uses_model for step in inputs.pipeline.steps)
280
+ return {
281
+ "schema": ENGINE_SNAPSHOT_SCHEMA,
282
+ "config": materials.config,
283
+ "config_fingerprint": config_fingerprint(materials.config),
284
+ "missing": ["resources"] if materials.collected.unresolved else [],
285
+ "provenance": _provenance(inputs, repo, engine, materials, steps_use_model),
286
+ "reproducibility": _reproducibility(inputs, materials, engine, steps_use_model),
287
+ }
288
+
289
+
290
+ def self_check(snapshot: Mapping[str, Any]) -> None:
291
+ """์—”์ง„ ์ž๊ธฐ ๊ฒ€์ฆ(D5) โ€” ์ด๋ฏธ ์žˆ๋Š” poc-bundle ํ•จ์ˆ˜๋งŒ ์“ด๋‹ค.
292
+
293
+ Raises:
294
+ SnapshotError: schema ๊ฐ€ ๋‹ค๋ฅด๊ฑฐ๋‚˜, ๊ทœ๋ฒ” ํ‚ค๊ฐ€ ๋น ์กŒ๊ฑฐ๋‚˜, ์ง€๋ฌธ์ด ์žฌ๊ณ„์‚ฐ๊ณผ ๋‹ค๋ฅผ ๋•Œ.
295
+ """
296
+ if snapshot.get("schema") != ENGINE_SNAPSHOT_SCHEMA:
297
+ raise SnapshotError(f"unknown snapshot schema {snapshot.get('schema')!r}")
298
+ config = snapshot.get("config")
299
+ if not isinstance(config, Mapping):
300
+ raise SnapshotError("snapshot.config must be an object")
301
+ gaps = completeness_gaps(config)
302
+ if gaps:
303
+ raise SnapshotError(f"snapshot.config lacks normative keys {gaps} (SPEC ยง3.4)")
304
+ expected = config_fingerprint(config)
305
+ if snapshot.get("config_fingerprint") != expected:
306
+ raise SnapshotError(
307
+ f"config_fingerprint {snapshot.get('config_fingerprint')!r} != recomputed {expected!r}"
308
+ )
309
+
310
+
311
+ def config_diff_keys(a: Mapping[str, Any], b: Mapping[str, Any]) -> list[str]:
312
+ """๋‘ `snapshot.config` ์˜ ๊ฐ’์ด ๋‹ค๋ฅธ ์ตœ์ƒ์œ„ ํ‚ค(์ •๋ ฌ) โ€” resume ๋“œ๋ฆฌํ”„ํŠธ ๋ฉ”์‹œ์ง€์šฉ(D17).
313
+
314
+ ๊ฐ’ ๋น„๊ต๋Š” JCS ์ง€๋ฌธ์œผ๋กœ ํ•œ๋‹ค(`0.0` โ‰ก `0`).
315
+ """
316
+ keys = set(a) | set(b)
317
+ return sorted(
318
+ k
319
+ for k in keys
320
+ if k not in a or k not in b or json_fingerprint(a[k]) != json_fingerprint(b[k])
321
+ )
322
+
323
+
324
+ def _rescore_providers(provenance: Mapping[str, Any], judge_provider: Any) -> dict[str, str | None]:
325
+ """์žฌ์ฑ„์ ์˜ ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ โ€” ์ถ”๋ก ์€ ๊ธฐ๋ก๋œ ์ด๋ฆ„, judge ๋Š” ๋„˜๊ฒจ๋ฐ›์€ ๊ฒƒ(์—†์œผ๋ฉด ๊ธฐ๋ก๋œ ์ด๋ฆ„).
326
+
327
+ Raises:
328
+ SnapshotError: ๊ธฐ๋ก๋œ ์ถ”๋ก  ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ์ด ์—†์–ด ๋Ÿฐํƒ€์ž„ ํ•ด์‹œ๋ฅผ ๋‹ค์‹œ ๋œฐ ์ˆ˜ ์—†์„ ๋•Œ.
329
+ """
330
+ recorded = provenance.get(PROVIDERS_KEY)
331
+ if not isinstance(recorded, Mapping) or not isinstance(recorded.get("inference"), str):
332
+ raise SnapshotError(
333
+ "snapshot.provenance.providers.inference is missing โ€” cannot recompute "
334
+ "runtime_impl_hash for the new evaluator"
335
+ )
336
+ judge = recorded.get("judge")
337
+ if judge_provider is not None:
338
+ judge = impl_hash.provider_module(judge_provider)
339
+ return {"inference": recorded["inference"], "judge": judge}
340
+
341
+
342
+ def with_evaluator(
343
+ snapshot: Mapping[str, Any],
344
+ evaluator_config: Mapping[str, Any] | None,
345
+ re_evaluated_at: str,
346
+ roots: ImplRoots | None = None,
347
+ judge_provider: Any = None,
348
+ ) -> dict[str, Any]:
349
+ """`runs eval`(์žฌ์ฑ„์ )์šฉ โ€” ํ‰๊ฐ€๊ธฐ ๋ธ”๋ก์„ ์ƒˆ ํ‰๊ฐ€๊ธฐ๋กœ ๋ฐ”๊พธ๊ณ  ์ง€๋ฌธ์„ ๋‹ค์‹œ ๊ณ„์‚ฐํ•œ **์ƒˆ** ์Šค๋ƒ…์ƒท(D11).
350
+
351
+ judge ์œ ๋ฌด๊ฐ€ ๋ฐ”๋€Œ๋ฉด judge ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ์ด ๋Ÿฐํƒ€์ž„ ํ•ด์‹œ์— ๋“ค์–ด๊ฐ€๊ฑฐ๋‚˜ ๋น ์ ธ์•ผ ํ•˜๋ฏ€๋กœ
352
+ `runtime_impl_hash`ยท`impl_modules["@runtime"]` ๋„ ๋นŒ๋“œ์™€ ๊ฐ™์€ ๊ทœ์น™(`_runtime`)์œผ๋กœ ๋‹ค์‹œ ๋œฌ๋‹ค โ€”
353
+ ์žฌ์ฑ„์  run ์ด ๊ฐ™์€ ๊ธฐ๋Šฅ ์„ค์ •์˜ ์ƒˆ run ๊ณผ ๊ฐ™์€ ์ง€๋ฌธ์„ ๊ฐ–๊ฒŒ. `judge_provider` ๋ฅผ ์•ˆ ์ฃผ๋ฉด ๋นŒ๋“œ ๋•Œ
354
+ ๊ธฐ๋กํ•œ judge ๋ชจ๋“ˆ์„ ์“ด๋‹ค. `provenance.re_evaluated_at` ์„ ๋‚จ๊ธฐ๊ณ  `deterministic` ๋„ ๋‹ค์‹œ ํŒ์ •ํ•œ๋‹ค.
355
+
356
+ Raises:
357
+ ValueError: ํ‰๊ฐ€๊ธฐ ์„ค์ •์ด ์ž˜๋ชป๋์„ ๋•Œ(`evaluator_block`).
358
+ SnapshotError: ๊ธฐ๋ก๋œ ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ์ด ์—†์–ด ๋Ÿฐํƒ€์ž„ ํ•ด์‹œ๋ฅผ ๋‹ค์‹œ ๋œฐ ์ˆ˜ ์—†์„ ๋•Œ.
359
+ """
360
+ roots = roots or impl_hash.default_roots()
361
+ updated = copy.deepcopy(dict(snapshot))
362
+ block, modules = evaluator_block(evaluator_config, roots)
363
+ provenance = updated.setdefault("provenance", {})
364
+ providers = _rescore_providers(provenance, judge_provider)
365
+ runtime_hash, runtime_modules = _runtime(providers, block["judge_model"], roots)
366
+ config = updated["config"]
367
+ config["evaluator_config"] = block
368
+ config["runtime_impl_hash"] = runtime_hash
369
+ updated["config_fingerprint"] = config_fingerprint(config)
370
+ impl_modules = provenance.setdefault("impl_modules", {})
371
+ impl_modules[EVALUATOR_MODULES_KEY] = modules
372
+ impl_modules[RUNTIME_MODULES_KEY] = runtime_modules
373
+ provenance[PROVIDERS_KEY] = providers
374
+ provenance["re_evaluated_at"] = re_evaluated_at
375
+ steps_use_model = provenance.get("steps_use_model")
376
+ if isinstance(steps_use_model, bool):
377
+ updated.setdefault("reproducibility", {})["deterministic"] = reproducibility.deterministic(
378
+ temperature=config.get("temperature"),
379
+ resolved_pipeline=config.get("resolved_pipeline") or {},
380
+ steps_use_model=steps_use_model,
381
+ judge_model=block["judge_model"],
382
+ )
383
+ return updated
384
+
385
+
386
+ __all__ = [
387
+ "AUTO",
388
+ "CONFIG_KEYS",
389
+ "DEFAULT_EVALUATOR_CONFIG",
390
+ "ENGINE_SNAPSHOT_SCHEMA",
391
+ "EngineSnapshotInputs",
392
+ "ImplRoots",
393
+ "PROVIDERS_KEY",
394
+ "SnapshotError",
395
+ "build_engine_snapshot",
396
+ "config_diff_keys",
397
+ "evaluator_block",
398
+ "self_check",
399
+ "with_evaluator",
400
+ ]
solar_eval/core/snapshot/dataset_block.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """run.json `dataset` ๋ธ”๋ก(engine-dataset/v1)๊ณผ lock ๋Œ€์กฐ (๊ณ„ํš 2026-09-24-phase1a S6 ยท D15).
2
+
3
+ lock ๋Œ€์กฐ์˜ ์ •์˜(๊ฒ€ํ†  F8) โ€” `matched = true` ๋Š” ์…‹์ด **๋ชจ๋‘** ๋งž์„ ๋•Œ๋‹ค:
4
+
5
+ 1. ์ฝ์€ ํŒŒ์ผ sha256 == lock ํ•ญ๋ชฉ `sha256`
6
+ 2. ์ด run ์˜ ํƒœ์Šคํฌ ์ด๋ฆ„ โˆˆ lock ํ•ญ๋ชฉ `dataset.tasks`
7
+ 3. ํƒœ์Šคํฌ์˜ `input_fields`ยท`golden_field`ยท`golden_fields` == lock ํ•ญ๋ชฉ์˜ `inputFields`ยท`goldenField`ยท
8
+ `goldenFields` (lock `datasetVersion` ์€ ์ด ์ŠคํŽ™์œผ๋กœ ๊ณ„์‚ฐ๋๋‹ค)
9
+
10
+ ํ•˜๋‚˜๋ผ๋„ ๋‹ค๋ฅด๋ฉด `matched: false` + `mismatch: ["sha256" | "task" | "fields", โ€ฆ]`. ๊ทธ๋ž˜์•ผ "์ „์ฒด์…‹ run ์˜
11
+ `dataset_version` == lock `datasetVersion`" ์ด ์กฐ์šฉํžˆ ๊นจ์ง€์ง€ ์•Š๋Š”๋‹ค.
12
+
13
+ ์ด ๋ชจ๋“ˆ์€ engine-filter ์˜ ํƒ€์ž…(`PreparedDataset`)์— ๊ธฐ๋Œ€์ง€ ์•Š๊ณ  **์›์‹œ ๊ฐ’**์„ ๋ฐ›๋Š”๋‹ค.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import copy
19
+ import logging
20
+ from collections.abc import Mapping, Sequence
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from poc_bundle.data.lockfile import read_lock
25
+ from poc_bundle.data.lock_model import DatasetsLock, LockDatasetEntry
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ ENGINE_DATASET_SCHEMA = "engine-dataset/v1"
30
+
31
+ #: config_dir ๊ธฐ์ค€ lock ์œ„์น˜ (`poc-bundle data push` ๊ฐ€ ์“ด๋‹ค).
32
+ LOCK_RELATIVE_PATH = Path("dataset") / "datasets.lock.yaml"
33
+
34
+ #: lock ๋Œ€์กฐ๊ฐ€ ์ ์šฉ๋˜๋Š” ๋กœ๋” โ€” lock ์€ ๋กœ์ปฌ ๋ฐ์ดํ„ฐ ํ™ˆ(cbs-data ์—์„œ pull ํ•œ datasets ์˜์—ญ)์˜ ์›์žฅ์ด๋‹ค.
35
+ LOCAL_SOURCE = "local"
36
+
37
+ MISMATCH_SHA = "sha256"
38
+ MISMATCH_TASK = "task"
39
+ MISMATCH_FIELDS = "fields"
40
+
41
+
42
+ def lock_path_for(config_dir: Path) -> Path:
43
+ return Path(config_dir) / LOCK_RELATIVE_PATH
44
+
45
+
46
+ def task_field_spec(
47
+ task_config: Mapping[str, Any],
48
+ ) -> tuple[list[str], str | None, dict[str, str] | None]:
49
+ """(input_fields, golden_field, golden_fields) โ€” lock ๊ณ„์‚ฐ(`poc_bundle.data.taskfields._golden`)๊ณผ
50
+ ๊ฐ™์€ ์ •๊ทœํ™”: ๋นˆ ๊ฐ’์€ None, ๋‘˜ ๋‹ค ์žˆ์œผ๋ฉด golden_field ๊ฐ€ ์ด๊ธด๋‹ค(์—”์ง„ runner ๊ทœ์น™).
51
+ """
52
+ inputs = list(task_config.get("input_fields") or [])
53
+ field = task_config.get("golden_field") or None
54
+ fields = task_config.get("golden_fields") or None
55
+ if field and fields:
56
+ fields = None
57
+ return inputs, field, dict(fields) if fields else None
58
+
59
+
60
+ def _display_path(path: Path, repo_root: Path | None) -> str:
61
+ if repo_root is not None:
62
+ try:
63
+ return path.resolve().relative_to(repo_root.resolve()).as_posix()
64
+ except ValueError:
65
+ pass
66
+ return path.resolve().as_posix()
67
+
68
+
69
+ def _find_entry(lock: DatasetsLock, dataset_path: str) -> LockDatasetEntry | None:
70
+ return next((e for e in lock.datasets if e.path == dataset_path), None)
71
+
72
+
73
+ def compare_entry(
74
+ entry: LockDatasetEntry, sha256: str, task_name: str, task_config: Mapping[str, Any]
75
+ ) -> list[str]:
76
+ """lock ํ•ญ๋ชฉ๊ณผ ์ด run ์˜ ๋ถˆ์ผ์น˜ ์‚ฌ์œ (์ˆœ์„œ ๊ณ ์ •: sha256 โ†’ task โ†’ fields). ๋นˆ ๋ชฉ๋ก = matched."""
77
+ mismatch: list[str] = []
78
+ if entry.sha256 != sha256:
79
+ mismatch.append(MISMATCH_SHA)
80
+ info = entry.dataset
81
+ if info is None:
82
+ # dataset ๋ธ”๋ก์ด ์—†๋Š” ํ•ญ๋ชฉ(.jsonl ์ด ์•„๋‹˜)์€ ํƒœ์Šคํฌยทํ•„๋“œ๋ฅผ ๋Œ€์กฐํ•  ์ˆ˜ ์—†๋‹ค.
83
+ return [*mismatch, MISMATCH_TASK, MISMATCH_FIELDS]
84
+ if task_name not in info.tasks:
85
+ mismatch.append(MISMATCH_TASK)
86
+ inputs, field, fields = task_field_spec(task_config)
87
+ lock_fields = dict(info.golden_fields) if info.golden_fields else None
88
+ if (inputs, field, fields) != (list(info.input_fields), info.golden_field, lock_fields):
89
+ mismatch.append(MISMATCH_FIELDS)
90
+ return mismatch
91
+
92
+
93
+ def lock_block(
94
+ *,
95
+ sha256: str,
96
+ dataset_path: str,
97
+ task_config: Mapping[str, Any],
98
+ config_dir: Path | None,
99
+ project_name: str,
100
+ repo_root: Path | None = None,
101
+ ) -> dict[str, Any] | None:
102
+ """`dataset.lock` โ€” lock ์ด ์—†๊ฑฐ๋‚˜ยทํ•ญ๋ชฉ์ด ์—†๊ฑฐ๋‚˜ยทํ”„๋กœ์ ํŠธ ์ด๋ฆ„์ด ๋‹ค๋ฅด๋ฉด None.
103
+
104
+ Raises:
105
+ poc_bundle.data.errors.LockError: lock ํŒŒ์ผ์ด ๊ทœ์น™(L01~L20)์„ ์–ด๊ธธ ๋•Œ โ€” ๊นจ์ง„ ์›์žฅ์œผ๋กœ ๋Œ€์กฐํ•œ
106
+ run ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค(`make data-lint` ๊ฐ€ ๊ฐ™์€ ์˜ค๋ฅ˜๋ฅผ ๋‚ธ๋‹ค).
107
+ """
108
+ if config_dir is None:
109
+ return None
110
+ path = lock_path_for(config_dir)
111
+ if not path.is_file():
112
+ return None
113
+ lock = read_lock(path)
114
+ if lock.project is not None and lock.project != project_name:
115
+ logger.warning(
116
+ "datasets.lock.yaml project %r != run project %r โ€” lock not used for %s",
117
+ lock.project,
118
+ project_name,
119
+ dataset_path,
120
+ )
121
+ return None
122
+ entry = _find_entry(lock, dataset_path)
123
+ if entry is None:
124
+ return None
125
+ mismatch = compare_entry(entry, sha256, str(task_config.get("name", "")), task_config)
126
+ info = entry.dataset
127
+ return {
128
+ "path": _display_path(path, repo_root),
129
+ "matched": not mismatch,
130
+ "mismatch": mismatch,
131
+ "dataset_version": info.dataset_version if info else None,
132
+ "source": info.source.model_dump(mode="json") if info and info.source else None,
133
+ "builder": info.builder.model_dump(mode="json") if info and info.builder else None,
134
+ }
135
+
136
+
137
+ def describe_dataset(
138
+ *,
139
+ sha256: str,
140
+ rows_total: int,
141
+ rows_selected: int,
142
+ selection: Mapping[str, Any] | None,
143
+ meta_fields: Sequence[str] | None,
144
+ task_config: Mapping[str, Any],
145
+ loader_cfg: Mapping[str, Any],
146
+ config_dir: Path | None,
147
+ project_name: str,
148
+ repo_root: Path | None = None,
149
+ ) -> dict[str, Any]:
150
+ """run.json `dataset` ๋ธ”๋ก. `selection` ์€ engine-filter ๊ฐ€ ์ •๊ทœํ™”ํ•œ JSON(`{where, limit}`).
151
+
152
+ Raises:
153
+ ValueError: ํ–‰ ์ˆ˜๊ฐ€ ์Œ์ˆ˜์ด๊ฑฐ๋‚˜ ์„ ํƒ ํ–‰์ด ์ „์ฒด๋ณด๋‹ค ๋งŽ์„ ๋•Œ(ํ˜ธ์ถœ์ž ๋ฒ„๊ทธ๋ฅผ ํฌ๊ฒŒ ์•Œ๋ฆฐ๋‹ค).
154
+ """
155
+ if rows_total < 0 or rows_selected < 0 or rows_selected > rows_total:
156
+ raise ValueError(f"invalid row counts: selected={rows_selected}, total={rows_total}")
157
+ source = loader_cfg.get("source", "huggingface")
158
+ dataset_path = str(task_config.get("dataset_path", ""))
159
+ lock = None
160
+ if source == LOCAL_SOURCE:
161
+ lock = lock_block(
162
+ sha256=sha256,
163
+ dataset_path=dataset_path,
164
+ task_config=task_config,
165
+ config_dir=config_dir,
166
+ project_name=project_name,
167
+ repo_root=repo_root,
168
+ )
169
+ return {
170
+ "schema": ENGINE_DATASET_SCHEMA,
171
+ "loader": {"source": source, "repo": loader_cfg.get("repo", "")},
172
+ "path": dataset_path,
173
+ "sha256": sha256,
174
+ "rows_total": rows_total,
175
+ "rows_selected": rows_selected,
176
+ "selection": copy.deepcopy(dict(selection)) if selection is not None else None,
177
+ "meta_fields": list(meta_fields) if meta_fields else None,
178
+ "lock": lock,
179
+ }
solar_eval/core/snapshot/environment.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์‹คํ–‰ ํ™˜๊ฒฝ ์žฌ๋ฃŒ โ€” ์˜์กด ๋ฒ„์ „ยท์—”์ง„ ๋ฒ„์ „ยท์—”์ง„ ๋ ˆํฌ ์ƒํƒœ (๊ณ„ํš 2026-09-24-phase1a D8ยทD9).
2
+
3
+ config ์— ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ(์ง€๋ฌธ ๋Œ€์ƒ)์€ ์ถœ๋ ฅ ๊ฒฝ๋กœ์— ์žˆ๋Š” **์ง์ ‘** ๋Ÿฐํƒ€์ž„ ์˜์กด๊ณผ ์—”์ง„ ๋ฒ„์ „ยทํŒŒ์ด์ฌ ๋ถ€๋ฒ„์ „๋ฟ์ด๋‹ค.
4
+ ์ „์ด ์„ค์น˜ ํํฌยทuv.lockยท์—”์ง„ ์ปค๋ฐ‹ยทํŠธ๋ฆฌ ํ•ด์‹œยทdirty ๋Š” provenance(์ง€๋ฌธ ๋ฐ–)๋กœ ๊ฐ„๋‹ค โ€” ๋ฌด๊ด€ํ•œ ์›Œํฌ์ŠคํŽ˜์ด์Šค
5
+ ๋ฉค๋ฒ„์˜ ๋ณ€ํ™”๊ฐ€ ์„ค์ • ์ง€๋ฌธ์„ ํ”๋“ค์ง€ ์•Š๊ฒŒ.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import subprocess
12
+ import sys
13
+ from collections.abc import Callable
14
+ from importlib import metadata
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from packaging.requirements import InvalidRequirement, Requirement
19
+ from packaging.utils import canonicalize_name
20
+ from poc_bundle.canonical import content_sha256
21
+
22
+ import solar_eval
23
+ from solar_eval._version import __version__
24
+ from solar_eval.core.snapshot.impl_hash import ImplRoots, engine_tree_sha256
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ #: ์—”์ง„ ๋ฐฐํฌ ์ด๋ฆ„.
29
+ ENGINE_DISTRIBUTION = "solar-eval"
30
+
31
+ #: ๋ชจ๋ธ ์ž…์ถœ๋ ฅ์— ๋‹ฟ์ง€ ์•Š๋Š” ์ง์ ‘ ์˜์กด โ€” CLI ํ‘œ์‹œยท์„ค์ • ๋กœ๋“œยท๋ฒˆ๋“ค ๋„๊ตฌ. `dependency_lock` ์—์„œ ๋บ€๋‹ค.
32
+ #: ์ƒˆ ์ง์ ‘ ์˜์กด์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ด ๋ชฉ๋ก์ด๋‚˜ ์ถœ๋ ฅ ์˜์กด ์ค‘ ํ•˜๋‚˜๋กœ ๋ถ„๋ฅ˜ํ•˜๊ธฐ ์ „๊นŒ์ง€ ํ…Œ์ŠคํŠธ๊ฐ€ ๊นจ์ง„๋‹ค(์˜๋„).
33
+ NON_OUTPUT_DEPENDENCIES = frozenset(
34
+ {"click", "tabulate", "rich", "pydantic-settings", "poc-bundle", "packaging"}
35
+ )
36
+
37
+ DEPENDENCY_LOCK_SCHEME = "direct-runtime/v1"
38
+
39
+ #: config_dir ์•ˆ์—์„œ ์ถœ๋ ฅ์— ์˜ํ–ฅ์„ ์ฃผ๋Š” ๊ฒฝ๋กœ(dirty ๋ฒ”์œ„). scripts/ยทinsights/ยทREADME ๋Š” ๋บ€๋‹ค(๊ฒ€ํ†  F12ยทm3).
40
+ CONFIG_DIRTY_ENTRIES = (
41
+ "project.yaml",
42
+ "prompts",
43
+ "pipelines",
44
+ "resources",
45
+ "dataset/datasets.lock.yaml",
46
+ )
47
+
48
+ GitRun = Callable[..., Any]
49
+
50
+
51
+ def _marker_env() -> dict[str, str]:
52
+ return {"extra": ""}
53
+
54
+
55
+ def direct_requirements(distribution: str = ENGINE_DISTRIBUTION) -> list[str]:
56
+ """๋ฐฐํฌ์˜ ์ง์ ‘ `Requires-Dist` ์ค‘ ํ˜„์žฌ ํ™˜๊ฒฝ์—์„œ ์ผœ์ง€๋Š” ๊ฒƒ(extra ์—†์Œ)์˜ ์ •๊ทœํ™” ์ด๋ฆ„(์ •๋ ฌ)."""
57
+ names: set[str] = set()
58
+ for raw in metadata.requires(distribution) or []:
59
+ try:
60
+ req = Requirement(raw)
61
+ except InvalidRequirement as exc:
62
+ raise ValueError(f"{distribution}: unparsable requirement {raw!r}: {exc}") from exc
63
+ if req.marker is not None and not req.marker.evaluate(_marker_env()):
64
+ continue
65
+ names.add(canonicalize_name(req.name))
66
+ return sorted(names)
67
+
68
+
69
+ def direct_runtime_dependencies() -> dict[str, str]:
70
+ """`dependency_lock.packages` โ€” ์ถœ๋ ฅ ๊ฒฝ๋กœ์— ์žˆ๋Š” ์ง์ ‘ ์˜์กด์˜ ์„ค์น˜ ๋ฒ„์ „."""
71
+ return {
72
+ name: metadata.version(name)
73
+ for name in direct_requirements()
74
+ if name not in NON_OUTPUT_DEPENDENCIES
75
+ }
76
+
77
+
78
+ def dependency_lock() -> dict[str, Any]:
79
+ return {
80
+ "scheme": DEPENDENCY_LOCK_SCHEME,
81
+ "root": ENGINE_DISTRIBUTION,
82
+ "packages": direct_runtime_dependencies(),
83
+ }
84
+
85
+
86
+ def installed_closure(distribution: str = ENGINE_DISTRIBUTION) -> dict[str, str | None]:
87
+ """์—”์ง„์—์„œ ๋‹ฟ๋Š” ์„ค์น˜ ํํฌ `{์ด๋ฆ„: ๋ฒ„์ „}` โ€” provenance ์ „์šฉ(ํ™˜๊ฒฝ ์žฌ๊ตฌ์„ฑยท๋””๋ฒ„๊ทธ).
88
+
89
+ ์ผœ์ง„ ์š”๊ตฌ์ธ๋ฐ ์„ค์น˜๋ผ ์žˆ์ง€ ์•Š์œผ๋ฉด ๋ฒ„์ „ None ์œผ๋กœ ๋‚จ๊ธด๋‹ค(์ˆจ๊ธฐ์ง€ ์•Š๋Š”๋‹ค).
90
+ """
91
+ seen: dict[str, str | None] = {}
92
+ queue: list[str] = [str(canonicalize_name(distribution))]
93
+ while queue:
94
+ name = queue.pop()
95
+ if name in seen:
96
+ continue
97
+ try:
98
+ seen[name] = metadata.version(name)
99
+ except metadata.PackageNotFoundError:
100
+ seen[name] = None
101
+ continue
102
+ queue.extend(n for n in direct_requirements(name) if n not in seen)
103
+ return dict(sorted(seen.items()))
104
+
105
+
106
+ def runner_version() -> dict[str, str]:
107
+ """`{"package", "version", "python": "<major>.<minor>"}` โ€” ํŒจ์น˜ ๋ฒ„์ „์€ ๋„ฃ์ง€ ์•Š๋Š”๋‹ค."""
108
+ return {
109
+ "package": ENGINE_DISTRIBUTION,
110
+ "version": __version__,
111
+ "python": f"{sys.version_info.major}.{sys.version_info.minor}",
112
+ }
113
+
114
+
115
+ def engine_src_dir() -> Path:
116
+ """์—”์ง„ ์†Œ์Šค ๋ฃจํŠธ(`packages/solar-eval-python/src`) โ€” `solar_eval.__file__` ๊ธฐ์ค€."""
117
+ return Path(solar_eval.__file__).resolve().parent.parent
118
+
119
+
120
+ def engine_repo(start: Path | None = None) -> Path | None:
121
+ """์—”์ง„ ์ฝ”๋“œ๊ฐ€ ๋“  git ๋ ˆํฌ โ€” `start`(๊ธฐ๋ณธ ์—”์ง„ ์†Œ์Šค)์—์„œ ์œ„๋กœ `.git` ์„ ์ฐพ๋Š”๋‹ค.
122
+
123
+ `cfg.repo_root` ๋ฅผ ์“ฐ์ง€ ์•Š๋Š”๋‹ค: `--projects-dir` ๋ฉด None ์ด ๋œ๋‹ค(๊ฒ€ํ†  F13). ์›ŒํฌํŠธ๋ฆฌ์˜ `.git` ์€
124
+ ํŒŒ์ผ์ด๋ผ ์กด์žฌ๋งŒ ๋ณธ๋‹ค.
125
+ """
126
+ here = (start or engine_src_dir()).resolve()
127
+ for candidate in (here, *here.parents):
128
+ if (candidate / ".git").exists():
129
+ return candidate
130
+ return None
131
+
132
+
133
+ def dirty_scope(
134
+ repo: Path, config_dir: Path | None, repo_managed: bool, src_dir: Path | None = None
135
+ ) -> list[str]:
136
+ """dirty ํŒ์ • ๋ฒ”์œ„(๋ ˆํฌ ์ƒ๋Œ€ POSIX, ์ •๋ ฌ). ๋ ˆํฌ ๋ฐ– ๊ฒฝ๋กœ๋Š” ๋„ฃ์ง€ ์•Š๋Š”๋‹ค."""
137
+ src = (src_dir or engine_src_dir()).resolve()
138
+ candidates = [src, src.parent / "pyproject.toml", repo / "uv.lock"]
139
+ if repo_managed and config_dir is not None:
140
+ candidates += [Path(config_dir).resolve() / entry for entry in CONFIG_DIRTY_ENTRIES]
141
+ root = repo.resolve()
142
+ scope = set()
143
+ for path in candidates:
144
+ try:
145
+ scope.add(path.resolve().relative_to(root).as_posix())
146
+ except ValueError:
147
+ logger.warning("dirty scope path outside the engine repo, skipped: %s", path)
148
+ return sorted(scope)
149
+
150
+
151
+ def _git(run: GitRun, repo: Path, *args: str) -> str | None:
152
+ """git ํ•œ ๋ฒˆ. ์‹คํŒจ(git ์—†์Œยท๋ ˆํฌ ์•„๋‹˜ยท๋น„์ •์ƒ ์ข…๋ฃŒ)๋Š” None โ€” ํ˜ธ์ถœ์ž๊ฐ€ null ๋กœ ๊ธฐ๋กํ•œ๋‹ค."""
153
+ try:
154
+ result = run(["git", "-C", str(repo), *args], capture_output=True, text=True, check=True)
155
+ except (OSError, subprocess.CalledProcessError) as exc:
156
+ logger.warning("git %s failed in %s: %s", " ".join(args[:2]), repo, exc)
157
+ return None
158
+ return result.stdout
159
+
160
+
161
+ def engine_state(
162
+ config_dir: Path | None,
163
+ repo_managed: bool,
164
+ *,
165
+ repo: Path | None = None,
166
+ src_dir: Path | None = None,
167
+ roots: ImplRoots | None = None,
168
+ run: GitRun = subprocess.run,
169
+ ) -> dict[str, Any]:
170
+ """`provenance.engine = {commit, tree_sha256, dirty, dirty_scope}` (D8).
171
+
172
+ `repo` ๋ฅผ ์•ˆ ์ฃผ๋ฉด `engine_repo()`. ๋ ˆํฌ๋ฅผ ๋ชป ์ฐพ๊ฑฐ๋‚˜ git ์ด ์‹คํŒจํ•˜๋ฉด `commit`ยท`dirty` ๋Š” null,
173
+ `tree_sha256` ์€ git ์—†์ด๋„ ๊ณ„์‚ฐ๋œ๋‹ค.
174
+ """
175
+ repo = repo if repo is not None else engine_repo(src_dir)
176
+ tree = engine_tree_sha256(roots)
177
+ if repo is None:
178
+ return {"commit": None, "tree_sha256": tree, "dirty": None, "dirty_scope": []}
179
+ scope = dirty_scope(repo, config_dir, repo_managed, src_dir)
180
+ head = _git(run, repo, "rev-parse", "HEAD")
181
+ # ๋นˆ ๋ฒ”์œ„๋กœ status ๋ฅผ ๋ถ€๋ฅด๋ฉด ๋ ˆํฌ ์ „์ฒด๋ฅผ ๋ณธ๋‹ค โ€” ๋ฒ”์œ„๊ฐ€ ์—†์œผ๋ฉด ํŒ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค(null).
182
+ # `--literal-pathspecs`: config_dir ์ด๋ฆ„์˜ `[`ยท`*`ยท`?` ๋ฅผ glob ์œผ๋กœ ์ฝ์ง€ ์•Š๊ฒŒ(๋ฒ”์œ„ ๋ฐ– ๋ณ€๊ฒฝ์ด dirty ๋กœ).
183
+ status = (
184
+ _git(
185
+ run,
186
+ repo,
187
+ "--literal-pathspecs",
188
+ "status",
189
+ "--porcelain",
190
+ "--untracked-files=normal",
191
+ "--",
192
+ *scope,
193
+ )
194
+ if scope
195
+ else None
196
+ )
197
+ return {
198
+ "commit": head.strip() if head else None,
199
+ "tree_sha256": tree,
200
+ "dirty": None if status is None else bool(status.strip()),
201
+ "dirty_scope": scope,
202
+ }
203
+
204
+
205
+ def uv_lock_sha256(repo: Path | None) -> str | None:
206
+ """๋ ˆํฌ `uv.lock` ์˜ sha256 โ€” provenance ์ „์šฉ(๋ชจ๋…ธ๋ ˆํฌ lock ์€ ๋ฌด๊ด€ํ•œ ๋ฉค๋ฒ„๋กœ๋„ ๋ฐ”๋€๋‹ค)."""
207
+ if repo is None:
208
+ return None
209
+ lock = repo / "uv.lock"
210
+ return content_sha256(lock) if lock.is_file() else None
solar_eval/core/snapshot/impl_hash.py ADDED
@@ -0,0 +1,365 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """๊ตฌํ˜„ ํ•ด์‹œ โ€” ๋ชจ๋“ˆ์˜ AST import ํํฌ ร— ํŒŒ์ผ ๋ฐ”์ดํŠธ (๊ณ„ํš 2026-09-24-phase1a D7).
2
+
3
+ ์„ธ ๊ฐˆ๋ž˜๊ฐ€ ๊ฐ™์€ ์žฌ๋ฃŒ๋ฅผ ์“ด๋‹ค:
4
+
5
+ - **์Šคํ…**(`step_impl_hashes`): ์Šคํ… ํด๋ž˜์Šค ๋ชจ๋“ˆ์—์„œ ์‹œ์ž‘ํ•œ import ํํฌ.
6
+ - **ํ‰๊ฐ€๊ธฐ**(`evaluator_impl_hash`): ํ‰๊ฐ€๊ธฐ ํด๋ž˜์Šค(ํ•ฉ์„ฑ์ด๋ฉด ํ•˜์œ„ ์ „๋ถ€)์˜ ๋ชจ๋“ˆ์—์„œ ์‹œ์ž‘ํ•œ ํํฌ.
7
+ - **๋Ÿฐํƒ€์ž„**(`runtime_impl_hash`): `RUNTIME_MODULES` + ์‹ค์ œ๋กœ ์“ด ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ โ€” ํํฌ๋ฅผ ๋Œ์ง€ ์•Š๊ณ 
8
+ ๋ชฉ๋ก์˜ ํŒŒ์ผ ๋ฐ”์ดํŠธ๋งŒ ๋ณธ๋‹ค(๋ชฉ๋ก์ด ๊ณง ์ •์˜).
9
+
10
+ ๋ชจ๋“ˆ โ†’ ํŒŒ์ผ์€ **์ด๋ฆ„์œผ๋กœ** ํ‘ผ๋‹ค(`ImplRoots`). ์ด๋ฏธ import ๋œ ๋ชจ๋“ˆ์˜ `__file__` ์ด๋‚˜ importlib spec ์„
11
+ ์“ฐ์ง€ ์•Š์œผ๋ฏ€๋กœ, ํ…Œ์ŠคํŠธ๊ฐ€ ๊ธฐ๋ณธ ๋ฃจํŠธ๋ฅผ tmp ์‚ฌ๋ณธ์œผ๋กœ ๋ฐ”๊ฟ” ๋ผ์šฐ๋ฉด ์žฌimport ์—†์ด "๊ตฌํ˜„๋งŒ ๋ฐ”๊พผ" ์ƒํ™ฉ์ด
12
+ ๋งŒ๋“ค์–ด์ง„๋‹ค. ํ•ด์‹œ๋Š” ๊ณ„์‚ฐํ•˜๋Š” ์ˆœ๊ฐ„ ๋””์Šคํฌ์˜ ํŒŒ์ผ์„ ์ฝ๋Š”๋‹ค.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import ast
18
+ import os
19
+ import sys
20
+ import sysconfig
21
+ from collections.abc import Iterable, Sequence
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from poc_bundle.canonical import content_sha256, json_fingerprint
27
+
28
+ import solar_eval
29
+
30
+ #: ์—”์ง„ ํŒจํ‚ค์ง€ ์ด๋ฆ„ โ€” ํŒจํ‚ค์ง€ ๋ฃจํŠธ์—์„œ๋Š” ์ด ์ด๋ฆ„ ์•„๋ž˜ ๋ชจ๋“ˆ๋งŒ ํ‘ผ๋‹ค(์„œ๋“œํŒŒํ‹ฐ ์ฐจ๋‹จ).
31
+ ENGINE_PACKAGE = "solar_eval"
32
+
33
+ #: ๋Ÿฌ๋„ˆยทํŒŒ์ดํ”„๋ผ์ธ ์‹คํ–‰๊ธฐยทํ”„๋กฌํ”„ํŠธ ์กฐ๋ฆฝ โ€” ๋ชจ๋“  ์Šคํ…์— ๋˜‘๊ฐ™์ด ์˜ํ–ฅ์„ ์ฃผ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ ๋ชจ๋“ˆ.
34
+ #: ์—ฌ๊ธฐ์— ์ด๋ฒˆ run ์ด ์“ฐ๋Š” ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ์„ ๋”ํ•ด `runtime_impl_hash` ๋ฅผ ๋œฌ๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ์ง‘ํ•ฉ์„ ๊ณ ์ •ํ•œ๋‹ค.
35
+ #: ๋Ÿฌ๋„ˆ์—์„œ ๋–ผ์–ด ๋‚ธ ๋„์šฐ๋ฏธ(์ž…๋ ฅยท์ •๋‹ต ์ถ”์ถœ `run_records`, ํ–‰ ์„ ํƒ `dataset_prep`ยท`row_filter`, ํŒŒ์ดํ”„๋ผ์ธ
36
+ #: ์ƒ์„ฑ `pipeline_factory`)๋„ ๋Ÿฌ๋„ˆ ๋กœ์ง์ด๋ผ ํ•จ๊ป˜ ๋ณธ๋‹ค โ€” `solar_eval.core.*` ๋Š” ์Šคํ…ยทํ‰๊ฐ€๊ธฐ ํํฌ์—์„œ ๋น ์ง€๋ฏ€๋กœ
37
+ #: ์—ฌ๊ธฐ ์—†์œผ๋ฉด ์–ด๋–ค ํ•ด์‹œ๋„ ์ด ํŒŒ์ผ๋“ค์„ ๋ณด์ง€ ์•Š๋Š”๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ๋Ÿฌ๋„ˆ์˜ core import ๊ฐ€ ์ „๋ถ€ ์—ฌ๊ธฐ ์žˆ๋Š”์ง€ ์ง€ํ‚จ๋‹ค.
38
+ RUNTIME_MODULES: tuple[str, ...] = (
39
+ "solar_eval.core.runner",
40
+ "solar_eval.core.dataset_loader",
41
+ "solar_eval.core.dataset_prep",
42
+ "solar_eval.core.row_filter",
43
+ "solar_eval.core.pipeline_factory",
44
+ "solar_eval.core.run_records",
45
+ "solar_eval.pipelines.pipeline",
46
+ "solar_eval.pipelines.registry",
47
+ "solar_eval.pipelines.base",
48
+ "solar_eval.evaluators.base",
49
+ "solar_eval.evaluators.registry",
50
+ "solar_eval.providers.base",
51
+ "solar_eval.models.prompt_version",
52
+ "solar_eval.models.sample",
53
+ )
54
+
55
+ #: ์Šคํ…ยทํ‰๊ฐ€๊ธฐ ํํฌ์—์„œ ๋นผ๋Š” ๊ณต์šฉ ๊ธฐ๋ฐ˜ โ€” ๋Ÿฐํƒ€์ž„ ํ•ด์‹œ๊ฐ€ ํ•œ ๋ฒˆ ๋ณด๊ฑฐ๋‚˜ ์ถœ๋ ฅ๊ณผ ๋ฌด๊ด€ํ•œ ๊ฒƒ.
56
+ _EXCLUDED_EXACT = frozenset(
57
+ {*RUNTIME_MODULES, "solar_eval.pipelines.errors", "solar_eval.config", "solar_eval._version"}
58
+ )
59
+ _EXCLUDED_PREFIXES = (
60
+ "solar_eval.providers.",
61
+ "solar_eval.cli.",
62
+ "solar_eval.stores.",
63
+ "solar_eval.core.",
64
+ )
65
+
66
+ #: ๊ตฌํ˜„ ํ•ด์‹œ ๋ผ๋ฒจ ์ ‘๋‘์–ด โ€” `pkg:<๋ฃจํŠธ ๊ธฐ์ค€ POSIX ๊ฒฝ๋กœ>`.
67
+ LABEL_PREFIX = "pkg:"
68
+ #: ๋ฃจํŠธ์—์„œ ํ’€ ์ˆ˜ ์—†๋Š” ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ(ํ…Œ์ŠคํŠธ mock ๋“ฑ)์˜ ๋ผ๋ฒจ ์ ‘๋‘์–ด. ๊ฐ’์€ null ๋กœ ๋“ค์–ด๊ฐ„๋‹ค.
69
+ UNRESOLVED_PREFIX = "unresolved:"
70
+
71
+
72
+ class ImplRootError(ValueError):
73
+ """๊ตฌํ˜„ ํ•ด์‹œ ๋ฃจํŠธ๋กœ ์“ธ ์ˆ˜ ์—†๋Š” ๋””๋ ‰ํ† ๋ฆฌ(์„œ๋“œํŒŒํ‹ฐยทํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์—ญ)."""
74
+
75
+
76
+ def _forbidden_bases() -> list[Path]:
77
+ """๋“ฑ๋ก ๋ฃจํŠธ๋กœ ๊ฑฐ๋ถ€ํ•˜๋Š” ์˜์—ญ โ€” site-packagesยทํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌยท์ธํ„ฐํ”„๋ฆฌํ„ฐ prefix."""
78
+ paths = sysconfig.get_paths()
79
+ bases = [paths.get(k) for k in ("purelib", "platlib", "stdlib")]
80
+ bases += [sys.prefix, sys.base_prefix]
81
+ return [Path(b).resolve() for b in bases if b]
82
+
83
+
84
+ def _is_within(path: Path, base: Path) -> bool:
85
+ try:
86
+ path.relative_to(base)
87
+ except ValueError:
88
+ return False
89
+ return True
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class ResolvedModule:
94
+ """์ด๋ฆ„์œผ๋กœ ํ‘ผ ๋ชจ๋“ˆ ํ•˜๋‚˜. `label` ์€ ์ง€๋ฌธ ํ‚ค(`pkg:solar_eval/pipelines/rules.py`)."""
95
+
96
+ name: str
97
+ path: Path
98
+ label: str
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class ImplRoots:
103
+ """๋ชจ๋“ˆ ์ด๋ฆ„ โ†’ ํŒŒ์ผ ํ•ด์„ ๋ฃจํŠธ.
104
+
105
+ - `package_parent`: `solar_eval` ํŒจํ‚ค์ง€์˜ ๋ถ€๋ชจ ๋””๋ ‰ํ† ๋ฆฌ. ์—ฌ๊ธฐ์„œ๋Š” `solar_eval.*` ์ด๋ฆ„๋งŒ ํ‘ผ๋‹ค โ€”
106
+ ์—”์ง„์ด site-packages ์— ์„ค์น˜๋ผ ์žˆ์–ด๋„ ์„œ๋“œํŒŒํ‹ฐ๊ฐ€ ํํฌ์— ๋“ค์–ด์˜ค์ง€ ์•Š๋Š”๋‹ค.
107
+ - `extra`: ํ…Œ์ŠคํŠธยทํ™•์žฅ์ด ๋ช…์‹œ ๋“ฑ๋กํ•œ ๋””๋ ‰ํ† ๋ฆฌ. ์—ฌ๊ธฐ์„œ๋Š” ์–ด๋–ค ์ด๋ฆ„์ด๋“  ํ‘ผ๋‹ค. ์„œ๋“œํŒŒํ‹ฐยทํ‘œ์ค€
108
+ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์˜์—ญ(`sysconfig` purelib/platlib/stdlib, `sys.prefix`, `sys.base_prefix`)์€ ๊ฑฐ๋ถ€ํ•œ๋‹ค
109
+ (uv ์›Œํฌ์ŠคํŽ˜์ด์Šค venv ๊ฐ€ ๋ ˆํฌ ์•ˆ์— ์žˆ์–ด "๋ ˆํฌ ์•ˆ" ๊ทœ์น™์ด๋ฉด httpx ๊ฐ€ ํํฌ์— ๋“ค์–ด์˜จ๋‹ค โ€” ๊ฒ€ํ†  F3).
110
+ """
111
+
112
+ package_parent: Path
113
+ extra: tuple[Path, ...] = field(default=())
114
+
115
+ def __post_init__(self) -> None:
116
+ forbidden = _forbidden_bases()
117
+ for root in self.extra:
118
+ resolved = Path(root).resolve()
119
+ if any(_is_within(resolved, base) for base in forbidden):
120
+ raise ImplRootError(
121
+ f"{root} is inside the interpreter/site-packages area โ€” third-party code is "
122
+ "covered by dependency_lock, not by implementation hashes"
123
+ )
124
+
125
+ @classmethod
126
+ def for_engine(cls, extra: Iterable[Path] = ()) -> ImplRoots:
127
+ """์ง€๊ธˆ import ๋œ `solar_eval` ํŒจํ‚ค์ง€์˜ ๋ถ€๋ชจ + ๋“ฑ๋ก ๋””๋ ‰ํ† ๋ฆฌ."""
128
+ parent = Path(solar_eval.__file__).resolve().parent.parent
129
+ return cls(parent, tuple(Path(p) for p in extra))
130
+
131
+ def with_extra(self, *dirs: Path) -> ImplRoots:
132
+ return ImplRoots(self.package_parent, (*self.extra, *(Path(d) for d in dirs)))
133
+
134
+ def resolve(self, name: str) -> ResolvedModule | None:
135
+ """๋ชจ๋“ˆ ์ด๋ฆ„์„ ํŒŒ์ผ๋กœ ํ‘ผ๋‹ค: `R/<a>/<b>.py` ๋˜๋Š” `R/<a>/<b>/__init__.py`. ์—†์œผ๋ฉด None."""
136
+ parts = name.split(".")
137
+ if not all(p.isidentifier() for p in parts):
138
+ return None
139
+ roots: list[Path] = []
140
+ if parts[0] == ENGINE_PACKAGE:
141
+ roots.append(self.package_parent)
142
+ roots.extend(self.extra)
143
+ for root in roots:
144
+ base = Path(root).joinpath(*parts)
145
+ for candidate in (base.with_suffix(".py"), base / "__init__.py"):
146
+ if candidate.is_file() and _exact_case(Path(root), candidate):
147
+ rel = candidate.relative_to(root).as_posix()
148
+ return ResolvedModule(name, candidate, LABEL_PREFIX + rel)
149
+ return None
150
+
151
+
152
+ def _exact_case(root: Path, candidate: Path) -> bool:
153
+ """`candidate` ์˜ ๊ฒฝ๋กœ ์„ฑ๋ถ„์ด ๋””๋ ‰ํ† ๋ฆฌ ๋ชฉ๋ก์˜ ์ด๋ฆ„๊ณผ **๋Œ€์†Œ๋ฌธ์ž๊นŒ์ง€** ๊ฐ™์€๊ฐ€.
154
+
155
+ macOS APFS ์˜ `is_file()` ์€ ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๋ฌด์‹œํ•œ๋‹ค โ€” `from pkg import Helper`(ํด๋ž˜์Šค)๊ฐ€
156
+ ํ˜•์ œ ๋ชจ๋“ˆ `pkg/helper.py` ๋กœ ์ž˜๋ชป ํ’€๋ ค, ๊ฐ™์€ ์ฝ”๋“œ๊ฐ€ Linux ์™€ ๋‹ค๋ฅธ ํํฌยท๋ผ๋ฒจ์„ ๋‚ธ๋‹ค.
157
+ ํŒŒ์ด์ฌ import ๋Š” ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜๋ฏ€๋กœ ์—ฌ๊ธฐ์„œ๋„ ๋ชฉ๋ก๊ณผ ์ •ํ™•ํžˆ ๋Œ€์กฐํ•ด Linux ์™€ ๊ฐ™๊ฒŒ ํ•œ๋‹ค.
158
+ """
159
+ current = root
160
+ for part in candidate.relative_to(root).parts:
161
+ try:
162
+ names = os.listdir(current)
163
+ except OSError:
164
+ return False
165
+ if part not in names:
166
+ return False
167
+ current = current / part
168
+ return True
169
+
170
+
171
+ # ๊ธฐ๋ณธ ๋ฃจํŠธ โ€” None ์ด๋ฉด `ImplRoots.for_engine()`. ํ…Œ์ŠคํŠธ๋Š” `set_default_roots` ๋กœ ์‚ฌ๋ณธ์„ ๋ผ์šด๋‹ค.
172
+ _DEFAULT_ROOTS: ImplRoots | None = None
173
+
174
+
175
+ def default_roots() -> ImplRoots:
176
+ """๊ธฐ๋ณธ ํ•ด์„ ๋ฃจํŠธ. `set_default_roots` ๋กœ ๋ฐ”๊พผ ๊ฐ’์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ."""
177
+ return _DEFAULT_ROOTS if _DEFAULT_ROOTS is not None else ImplRoots.for_engine()
178
+
179
+
180
+ def set_default_roots(roots: ImplRoots | None) -> ImplRoots | None:
181
+ """๊ธฐ๋ณธ ๋ฃจํŠธ๋ฅผ ๋ฐ”๊พธ๊ณ  ์ด์ „ ๊ฐ’์„ ๋Œ๋ ค์ค€๋‹ค(๋ณต์›์šฉ). None ์ด๋ฉด ์—”์ง„ ๊ธฐ๋ณธ์œผ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค."""
182
+ global _DEFAULT_ROOTS
183
+ previous = _DEFAULT_ROOTS
184
+ _DEFAULT_ROOTS = roots
185
+ return previous
186
+
187
+
188
+ def is_excluded(module: ResolvedModule) -> bool:
189
+ """์Šคํ…ยทํ‰๊ฐ€๊ธฐ ํํฌ์—์„œ ๋นผ๋Š”๊ฐ€ โ€” ๋Ÿฐํƒ€์ž„ ๋ชฉ๋กยท๊ณต์šฉ ๊ธฐ๋ฐ˜ยท๋ชจ๋“  ํŒจํ‚ค์ง€ `__init__`."""
190
+ if module.path.name == "__init__.py":
191
+ return True
192
+ if module.name in _EXCLUDED_EXACT:
193
+ return True
194
+ return module.name.startswith(_EXCLUDED_PREFIXES)
195
+
196
+
197
+ def _package_of(module: ResolvedModule) -> str:
198
+ if module.path.name == "__init__.py":
199
+ return module.name
200
+ return module.name.rpartition(".")[0]
201
+
202
+
203
+ def _absolute_from(module: ResolvedModule, node: ast.ImportFrom) -> str | None:
204
+ """`from ..x import y` ์˜ ๊ธฐ์ค€ ๋ชจ๋“ˆ ์ด๋ฆ„. ์ตœ์ƒ์œ„ ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด None."""
205
+ if node.level == 0:
206
+ return node.module
207
+ package = _package_of(module).split(".") if _package_of(module) else []
208
+ keep = len(package) - (node.level - 1)
209
+ if keep < 0 or (keep == 0 and not node.module):
210
+ return None
211
+ base = package[:keep]
212
+ if node.module:
213
+ base += node.module.split(".")
214
+ return ".".join(base) if base else None
215
+
216
+
217
+ def imported_names(module: ResolvedModule, roots: ImplRoots) -> list[str]:
218
+ """๋ชจ๋“ˆ ์†Œ์Šค์˜ **๋ชจ๋“ ** import(ํ•จ์ˆ˜ ์•ˆยทTYPE_CHECKING ๋ธ”๋ก ํฌํ•จ)๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋ชจ๋“ˆ ์ด๋ฆ„.
219
+
220
+ `from P import X` ๋Š” `P.X` ๊ฐ€ ๋ชจ๋“ˆ์ด๋ฉด ๊ทธ ๋ชจ๋“ˆ, ์•„๋‹ˆ๋ฉด `P`.
221
+ """
222
+ tree = ast.parse(module.path.read_bytes(), filename=str(module.path))
223
+ names: list[str] = []
224
+ for node in ast.walk(tree):
225
+ if isinstance(node, ast.Import):
226
+ names.extend(alias.name for alias in node.names)
227
+ elif isinstance(node, ast.ImportFrom):
228
+ base = _absolute_from(module, node)
229
+ if base is None:
230
+ continue
231
+ for alias in node.names:
232
+ candidate = f"{base}.{alias.name}"
233
+ is_module = alias.name != "*" and roots.resolve(candidate) is not None
234
+ names.append(candidate if is_module else base)
235
+ return names
236
+
237
+
238
+ def import_closure(
239
+ start: Iterable[str], roots: ImplRoots, *, exclude: bool = True
240
+ ) -> dict[str, ResolvedModule]:
241
+ """์‹œ์ž‘ ๋ชจ๋“ˆ๋“ค์—์„œ ๋ฃจํŠธ ์•ˆ์œผ๋กœ ๋‹ฟ๋Š” import ํํฌ. ์‹œ์ž‘ ๋ชจ๋“ˆ์€ ์ œ์™ธ ๊ทœ์น™๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋“ค์–ด๊ฐ„๋‹ค.
242
+
243
+ Raises:
244
+ ValueError: ์‹œ์ž‘ ๋ชจ๋“ˆ์„ ๋ฃจํŠธ์—์„œ ํ’€ ์ˆ˜ ์—†์„ ๋•Œ(๊ตฌํ˜„์„ ๋ณผ ์ˆ˜ ์—†๋Š” ์Šคํ…ยทํ‰๊ฐ€๊ธฐ).
245
+ """
246
+ closure: dict[str, ResolvedModule] = {}
247
+ queue: list[ResolvedModule] = []
248
+ for name in start:
249
+ resolved = roots.resolve(name)
250
+ if resolved is None:
251
+ raise ValueError(
252
+ f"module {name!r} is not under the implementation roots "
253
+ f"({roots.package_parent}, extra={list(roots.extra)}) โ€” register its directory"
254
+ )
255
+ queue.append(resolved)
256
+ while queue:
257
+ module = queue.pop()
258
+ if module.name in closure:
259
+ continue
260
+ closure[module.name] = module
261
+ for name in imported_names(module, roots):
262
+ found = roots.resolve(name)
263
+ if found is None or found.name in closure:
264
+ continue
265
+ if exclude and is_excluded(found):
266
+ continue
267
+ queue.append(found)
268
+ return closure
269
+
270
+
271
+ def impl_hash(modules: Iterable[ResolvedModule]) -> str:
272
+ """`json_fingerprint({label: content_sha256(ํŒŒ์ผ ๋ฐ”์ดํŠธ)})` โ€” ์ฃผ์„ ๋ณ€๊ฒฝ๋„ ํ•ด์‹œ๋ฅผ ๋ฐ”๊พผ๋‹ค(์˜๋„)."""
273
+ return json_fingerprint({m.label: content_sha256(m.path) for m in modules})
274
+
275
+
276
+ def closure_hash(start: Iterable[str], roots: ImplRoots) -> tuple[str, list[str]]:
277
+ """(ํํฌ ํ•ด์‹œ, ์ •๋ ฌ๋œ ๋ผ๋ฒจ ๋ชฉ๋ก)."""
278
+ closure = import_closure(start, roots)
279
+ return impl_hash(closure.values()), sorted(m.label for m in closure.values())
280
+
281
+
282
+ def step_impl_hashes(
283
+ pipeline: Any, roots: ImplRoots | None = None
284
+ ) -> tuple[dict[str, str], dict[str, list[str]]]:
285
+ """์ƒ์„ฑ๋œ `Pipeline` ์˜ ์Šคํ…๋งˆ๋‹ค (ํ‚ค โ†’ ํ•ด์‹œ, ํ‚ค โ†’ ํํฌ ๋ผ๋ฒจ). ํ‚ค๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์กฐํšŒ ์ด๋ฆ„.
286
+
287
+ single_step ์˜ ์•”๋ฌต `llm_completion` ๋„ ํŒŒ์ดํ”„๋ผ์ธ ๊ฐ์ฒด์— ์žˆ์œผ๋ฏ€๋กœ ๋“ค์–ด๊ฐ„๋‹ค(๊ฒ€ํ†  F10).
288
+ """
289
+ from solar_eval.pipelines.registry import step_key
290
+
291
+ roots = roots or default_roots()
292
+ hashes: dict[str, str] = {}
293
+ modules: dict[str, list[str]] = {}
294
+ for step in pipeline.steps:
295
+ key = step_key(step.config)
296
+ if key in hashes:
297
+ continue
298
+ hashes[key], modules[key] = closure_hash([type(step).__module__], roots)
299
+ return dict(sorted(hashes.items())), dict(sorted(modules.items()))
300
+
301
+
302
+ def _evaluator_modules(evaluator: Any) -> list[str]:
303
+ names = [type(evaluator).__module__]
304
+ for child in evaluator.sub_evaluators():
305
+ names.extend(_evaluator_modules(child))
306
+ return names
307
+
308
+
309
+ def evaluator_impl_hash(evaluator: Any, roots: ImplRoots | None = None) -> tuple[str, list[str]]:
310
+ """ํ‰๊ฐ€๊ธฐ(ํ•ฉ์„ฑ์ด๋ฉด ํ•˜์œ„ ์ „๋ถ€) ๋ชจ๋“ˆ์—์„œ ์‹œ์ž‘ํ•œ ํํฌ์˜ (ํ•ด์‹œ, ๋ผ๋ฒจ)."""
311
+ return closure_hash(_evaluator_modules(evaluator), roots or default_roots())
312
+
313
+
314
+ def provider_module(provider: Any) -> str:
315
+ """ํ”„๋กœ๋ฐ”์ด๋”(์ธ์Šคํ„ด์Šค๋‚˜ ํด๋ž˜์Šค)๊ฐ€ ์ •์˜๋œ ๋ชจ๋“ˆ ์ด๋ฆ„."""
316
+ return provider.__module__ if isinstance(provider, type) else type(provider).__module__
317
+
318
+
319
+ def runtime_impl_hash(
320
+ providers: Sequence[Any], roots: ImplRoots | None = None
321
+ ) -> tuple[str, list[str]]:
322
+ """`RUNTIME_MODULES` + ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ์˜ ํŒŒ์ผ ๋ฐ”์ดํŠธ ์ง€๋ฌธ (ํํฌ ์—†์Œ).
323
+
324
+ ํ”„๋กœ๋ฐ”์ด๋”๋Š” ์ธ์Šคํ„ด์Šค๋‚˜ ํด๋ž˜์Šค๋ฅผ ๋ฐ›๋Š”๋‹ค. ๋ฃจํŠธ์—์„œ ํ’€ ์ˆ˜ ์—†๋Š” ํ”„๋กœ๋ฐ”์ด๋” ๋ชจ๋“ˆ(ํ…Œ์ŠคํŠธ mock ๋“ฑ)์€
325
+ `unresolved:<๋ชจ๋“ˆ>` ํ‚ค์— null ๋กœ ๋“ค์–ด๊ฐ„๋‹ค โ€” ๋ชจ๋ฅด๋Š” ์ฝ”๋“œ๋ฅผ ์กฐ์šฉํžˆ ๋นผ์ง€ ์•Š๋Š”๋‹ค.
326
+
327
+ Raises:
328
+ ValueError: `RUNTIME_MODULES` ์ค‘ ํ•˜๋‚˜๋ฅผ ๋ฃจํŠธ์—์„œ ํ’€ ์ˆ˜ ์—†์„ ๋•Œ.
329
+ """
330
+ return runtime_impl_hash_for_modules([provider_module(p) for p in providers], roots)
331
+
332
+
333
+ def runtime_impl_hash_for_modules(
334
+ provider_modules: Iterable[str], roots: ImplRoots | None = None
335
+ ) -> tuple[str, list[str]]:
336
+ """`runtime_impl_hash` ์˜ ๋ชจ๋“ˆ ์ด๋ฆ„ํŒ โ€” ์žฌ์ฑ„์ (`with_evaluator`)์ด ๊ธฐ๋ก๋œ ๋ชจ๋“ˆ ์ด๋ฆ„์œผ๋กœ ๋‹ค์‹œ ๋œฌ๋‹ค."""
337
+ roots = roots or default_roots()
338
+ entries: dict[str, str | None] = {}
339
+ for name in RUNTIME_MODULES:
340
+ resolved = roots.resolve(name)
341
+ if resolved is None:
342
+ raise ValueError(f"runtime module {name!r} not found under {roots.package_parent}")
343
+ entries[resolved.label] = content_sha256(resolved.path)
344
+ for name in sorted(set(provider_modules)):
345
+ resolved = roots.resolve(name)
346
+ if resolved is None:
347
+ entries[UNRESOLVED_PREFIX + name] = None
348
+ else:
349
+ entries[resolved.label] = content_sha256(resolved.path)
350
+ return json_fingerprint(entries), sorted(entries)
351
+
352
+
353
+ def engine_tree_sha256(roots: ImplRoots | None = None) -> str:
354
+ """์—”์ง„ ํŒจํ‚ค์ง€ ํŠธ๋ฆฌ(`solar_eval/**/*.py`, `__pycache__` ์ œ์™ธ)์˜ ํŒŒ์ผ ๋ฐ”์ดํŠธ ์ง€๋ฌธ โ€” provenance ์šฉ.
355
+
356
+ git ์—†์ด๋„ ๊ณ„์‚ฐ๋œ๋‹ค. ํ‚ค๋Š” ํŒจํ‚ค์ง€ ๋ถ€๋ชจ ๊ธฐ์ค€ POSIX ๊ฒฝ๋กœ(`solar_eval/core/runner.py`).
357
+ """
358
+ parent = (roots or default_roots()).package_parent
359
+ package = Path(parent) / ENGINE_PACKAGE
360
+ entries = {
361
+ path.relative_to(parent).as_posix(): content_sha256(path)
362
+ for path in sorted(package.rglob("*.py"))
363
+ if "__pycache__" not in path.relative_to(parent).parts
364
+ }
365
+ return json_fingerprint(entries)
solar_eval/core/snapshot/reproducibility.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """run.json `snapshot.reproducibility` โ€” ๋‹ค์‹œ ๋Œ๋ฆด ์ˆ˜ ์žˆ๋Š”๊ฐ€ยท๊ฒฐ์ •์ ์ธ๊ฐ€ (๊ณ„ํš 2026-09-24-phase1a D10ยทD17).
2
+
3
+ ๋‘ ํ•„๋“œ ๋ชจ๋‘ run.json(๊ณผ ๋ฒˆ๋“ค `config_snapshot.json` provenance)์—๋งŒ ๋‘”๋‹ค. ๋งค๋‹ˆํŽ˜์ŠคํŠธ ๋…ธ์ถœ์€
4
+ `poc-bundle/v2` ๋กœ ๋ฏธ๋ฃฌ๋‹ค(Q8).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ from collections.abc import Iterable, Mapping, Sequence
11
+ from typing import Any
12
+
13
+ from solar_eval.pipelines.pipeline import ResourceRef
14
+
15
+ BLOCKER_ENGINE_DIRTY = "engine_dirty"
16
+ BLOCKER_CONFIG_OUTSIDE_REPO = "config_outside_repo"
17
+ BLOCKER_DATASET_NOT_LOCKED = "dataset_not_locked"
18
+ BLOCKER_DATASET_LOCK_MISMATCH = "dataset_lock_mismatch"
19
+ BLOCKER_DATASET_REMOTE = "dataset_remote_source"
20
+ BLOCKER_RESUMED_ENGINE_CHANGE = "resumed_with_engine_change"
21
+ BLOCKER_RESOURCE_UNRESOLVED = "resource_unresolved:"
22
+ BLOCKER_RESOURCE_OUTSIDE_REPO = "resource_outside_repo:"
23
+
24
+ _LOCAL_SOURCE = "local"
25
+
26
+
27
+ def _dataset_blockers(dataset: Mapping[str, Any] | None) -> list[str]:
28
+ if dataset is None:
29
+ return [BLOCKER_DATASET_NOT_LOCKED]
30
+ if dataset.get("loader", {}).get("source") != _LOCAL_SOURCE:
31
+ return [BLOCKER_DATASET_REMOTE]
32
+ lock = dataset.get("lock")
33
+ if lock is None:
34
+ return [BLOCKER_DATASET_NOT_LOCKED]
35
+ if not lock.get("matched"):
36
+ return [BLOCKER_DATASET_LOCK_MISMATCH]
37
+ return []
38
+
39
+
40
+ def _resource_blockers(refs: Mapping[str, ResourceRef]) -> list[str]:
41
+ blockers: list[str] = []
42
+ for label, ref in sorted(refs.items()):
43
+ if ref.path is None:
44
+ blockers.append(BLOCKER_RESOURCE_UNRESOLVED + label)
45
+ elif ref.kind != "config":
46
+ # data: ์ž์›(PMI ๋“ฑ)์€ ๋ ˆํฌ ๋ฐ– ๋ฐ์ดํ„ฐ ํ™ˆ์ด๊ณ  lock ๋Œ€์ƒ๋„ ์•„๋‹ˆ๋‹ค โ€” ์ปค๋ฐ‹์œผ๋กœ ๋ณต์›ํ•  ์ˆ˜ ์—†๋‹ค.
47
+ blockers.append(BLOCKER_RESOURCE_OUTSIDE_REPO + label)
48
+ return blockers
49
+
50
+
51
+ def _verdict(blockers: Sequence[str], engine_dirty: bool | None) -> bool | None:
52
+ if blockers:
53
+ return False
54
+ return None if engine_dirty is None else True
55
+
56
+
57
+ def rerunnable(
58
+ *,
59
+ engine_dirty: bool | None,
60
+ repo_managed: bool,
61
+ dataset: Mapping[str, Any] | None,
62
+ resources: Mapping[str, ResourceRef],
63
+ ) -> tuple[bool | None, list[str]]:
64
+ """(rerunnable, ๋ง‰๋Š” ์ด์œ  ๋ชฉ๋ก). true = ๊ธฐ๋ก๋œ ์ •๋ณด๋งŒ์œผ๋กœ ๊ฐ™์€ ์ž…๋ ฅ์„ ๋‹ค์‹œ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
65
+
66
+ ๋ง‰๋Š” ์ด์œ ๊ฐ€ ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด false, ์—†๋Š”๋ฐ `engine_dirty` ๊ฐ€ null(git ๋ถˆ๋ช…)์ด๋ฉด null.
67
+ """
68
+ blockers: list[str] = []
69
+ if engine_dirty:
70
+ blockers.append(BLOCKER_ENGINE_DIRTY)
71
+ if not repo_managed:
72
+ blockers.append(BLOCKER_CONFIG_OUTSIDE_REPO)
73
+ blockers += _dataset_blockers(dataset)
74
+ blockers += _resource_blockers(resources)
75
+ return _verdict(blockers, engine_dirty), blockers
76
+
77
+
78
+ def _positive(value: Any) -> bool:
79
+ return isinstance(value, int | float) and not isinstance(value, bool) and value > 0
80
+
81
+
82
+ def _step_temperatures(step: Mapping[str, Any]) -> Iterable[Any]:
83
+ yield step.get("temperature")
84
+ temps = step.get("repeat_temperatures")
85
+ if isinstance(temps, Sequence) and not isinstance(temps, str | bytes):
86
+ yield from temps
87
+
88
+
89
+ def deterministic(
90
+ *,
91
+ temperature: Any,
92
+ resolved_pipeline: Mapping[str, Any],
93
+ steps_use_model: bool,
94
+ judge_model: str | None,
95
+ ) -> bool | None:
96
+ """true = ๋ชจ๋ธ ํ˜ธ์ถœ์ด ์ „ํ˜€ ์—†๋‹ค ยท false = ์ƒ˜ํ”Œ๋ง ์˜จ๋„ > 0 ์ด ํ™•์ธ๋จ ยท null = ์˜จ๋„ 0 ์˜ LLM ํ˜ธ์ถœ.
97
+
98
+ API ๋Š” ์˜จ๋„ 0 ์—์„œ๋„ ๊ฒฐ์ •์ ์ด๋ผ๋Š” ๋ณด์žฅ์ด ์—†์œผ๋ฏ€๋กœ ๊ทธ ๊ฒฝ์šฐ๋Š” ๋ชจ๋ฅธ๋‹ค(null, Q7).
99
+ """
100
+ if not steps_use_model and judge_model is None:
101
+ return True
102
+ temps: list[Any] = [temperature]
103
+ for step in resolved_pipeline.get("steps") or []:
104
+ if isinstance(step, Mapping):
105
+ temps.extend(_step_temperatures(step))
106
+ if any(_positive(t) for t in temps):
107
+ return False
108
+ return None
109
+
110
+
111
+ def _or3(values: Iterable[bool | None]) -> bool | None:
112
+ """ํ•˜๋‚˜๋ผ๋„ true โ†’ true, ์•„๋‹ˆ๊ณ  ํ•˜๋‚˜๋ผ๋„ null โ†’ null, ๋ชจ๋‘ false โ†’ false."""
113
+ items = list(values)
114
+ if any(v is True for v in items):
115
+ return True
116
+ if any(v is None for v in items):
117
+ return None
118
+ return False
119
+
120
+
121
+ def merge_resume(snapshot: Mapping[str, Any], resume_engine: Mapping[str, Any], at: str) -> dict:
122
+ """resume 1ํšŒ๋ฅผ ์Šค๋ƒ…์ƒท์— ๋ฐ˜์˜ํ•œ **์ƒˆ** ์Šค๋ƒ…์ƒท(์ž…๋ ฅ์€ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค, D17).
123
+
124
+ - `provenance.resumes` ์— `{at, engine: {commit, tree_sha256, dirty}}` ๋ฅผ ๋ง๋ถ™์ธ๋‹ค.
125
+ - `reproducibility.engine_dirty` = ์‹œ์ž‘ยท๋ชจ๋“  resume ์˜ dirty OR.
126
+ - ์—”์ง„ ํŠธ๋ฆฌ ํ•ด์‹œ๊ฐ€ ์‹œ์ž‘๊ณผ ๋‹ค๋ฅด๋ฉด `resumed_with_engine_change` ๋ฅผ ๋ง‰๋Š” ์ด์œ ์— ๋”ํ•œ๋‹ค.
127
+ configยท์ง€๋ฌธ์€ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€” ์„ค์ • ๋“œ๋ฆฌํ”„ํŠธ๋Š” ํ˜ธ์ถœ์ž๊ฐ€ ์ง€๋ฌธ ๋น„๊ต๋กœ ๋จผ์ € ๋ง‰๋Š”๋‹ค.
128
+ """
129
+ merged = copy.deepcopy(dict(snapshot))
130
+ provenance = merged.setdefault("provenance", {})
131
+ start = provenance.get("engine") or {}
132
+ entry = {
133
+ "at": at,
134
+ "engine": {k: resume_engine.get(k) for k in ("commit", "tree_sha256", "dirty")},
135
+ }
136
+ resumes = [*provenance.get("resumes", []), entry]
137
+ provenance["resumes"] = resumes
138
+ repro = merged.setdefault("reproducibility", {})
139
+ dirty = _or3([start.get("dirty"), *(r["engine"]["dirty"] for r in resumes)])
140
+ blockers = list(repro.get("rerunnable_blockers", []))
141
+ if dirty and BLOCKER_ENGINE_DIRTY not in blockers:
142
+ blockers.insert(0, BLOCKER_ENGINE_DIRTY)
143
+ changed = any(r["engine"]["tree_sha256"] != start.get("tree_sha256") for r in resumes)
144
+ if changed and BLOCKER_RESUMED_ENGINE_CHANGE not in blockers:
145
+ blockers.append(BLOCKER_RESUMED_ENGINE_CHANGE)
146
+ repro["engine_dirty"] = dirty
147
+ repro["rerunnable_blockers"] = blockers
148
+ repro["rerunnable"] = _verdict(blockers, dirty)
149
+ return merged
solar_eval/core/snapshot/resources.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """์Šค๋ƒ…์ƒท์˜ ํŒŒ์ผ ์žฌ๋ฃŒ โ€” ์Šคํ…์ด ์ฝ์„ ์ž์›(`resources`)๊ณผ ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ(`prompt_assets`).
2
+
3
+ ๋‘˜ ๋‹ค **๋‚ด์šฉ**๋งŒ ๋ณธ๋‹ค: ๋””๋ ‰ํ† ๋ฆฌยทํŒŒ์ผ ์ด๋ฆ„์€ ๊ฐ’์— ๋“ค์–ด๊ฐ€์ง€ ์•Š์œผ๋ฏ€๋กœ(ํ”„๋กฌํ”„ํŠธ๋Š” ๋””๋ ‰ํ† ๋ฆฌ ๊ธฐ์ค€ ์ƒ๋Œ€๊ฒฝ๋กœ๊ฐ€
4
+ ํ‚ค) ์ด๋ฆ„๋งŒ ๋ฐ”๊พผ ์‚ฌ๋ณธ์€ ๊ฐ™์€ ๊ฐ’์ด๋‹ค (๊ณ„ํš 2026-09-24-phase1a D6).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from poc_bundle.canonical import content_sha256
14
+
15
+ from solar_eval.core.fingerprint import iter_prompt_asset_files
16
+ from solar_eval.pipelines.pipeline import ResourceRef
17
+
18
+ #: ๋‹จ์ผ YAML ํ”„๋กฌํ”„ํŠธ ํŒŒ์ผ์˜ `prompt_assets` ํ‚ค โ€” ํŒŒ์ผ ์ด๋ฆ„์ด ์ง€๋ฌธ์— ๋“ค์–ด๊ฐ€์ง€ ์•Š๊ฒŒ.
19
+ SINGLE_FILE_KEY = "@file"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CollectedResources:
24
+ """`resources` ๊ฐ’๊ณผ ๊ทธ ํ•ด์„ ๊ฒฐ๊ณผ.
25
+
26
+ Attributes:
27
+ hashes: `{๋ผ๋ฒจ: sha256_64 | None}`(๋ผ๋ฒจ ์ •๋ ฌ). None = ํ•ด์„ ๋ชป ํ•จ(`hf:` ์„ ์–ธ ๋“ฑ).
28
+ refs: ๋ผ๋ฒจ โ†’ ์›๋ž˜ `ResourceRef`(์žฌํ˜„ ํŒ์ •์ด ์ข…๋ฅ˜๋ฅผ ๋ณธ๋‹ค).
29
+ """
30
+
31
+ hashes: dict[str, str | None]
32
+ refs: dict[str, ResourceRef]
33
+
34
+ @property
35
+ def unresolved(self) -> list[str]:
36
+ """๊ฐ’์ด None ์ธ ๋ผ๋ฒจ โ€” ํ•˜๋‚˜๋ผ๋„ ์žˆ์œผ๋ฉด `snapshot.missing` ์— `resources` ๊ฐ€ ์˜ค๋ฅธ๋‹ค."""
37
+ return [label for label, value in self.hashes.items() if value is None]
38
+
39
+
40
+ def collect_resources(pipeline: Any) -> CollectedResources:
41
+ """`Pipeline` ์ธ์Šคํ„ด์Šค์˜ ์Šคํ…๋งˆ๋‹ค `resource_refs()` ๋ฅผ ๋ชจ์•„ ํŒŒ์ผ sha256 ์„ ๋œฌ๋‹ค.
42
+
43
+ ๊ฐ™์€ ๋ผ๋ฒจ(๊ฐ™์€ ์‚ฌ์ „์„ ๋‘ ์Šคํ…์ด ์“ฐ๋Š” prod ํŒŒ์ดํ”„๋ผ์ธ)์€ ํ•œ ๋ฒˆ๋งŒ ์ ๋Š”๋‹ค. `hf:` ์ž์›์€ ๋„คํŠธ์›Œํฌ๋ฅผ
44
+ ํƒ€์ง€ ์•Š๊ณ  null ์ด๋‹ค. ์„ ์–ธํ–ˆ๋Š”๋ฐ ๋กœ์ปฌ์— ์—†๋Š” ์ž์›์€ ์Šคํ…์˜ `resource_refs()` ๊ฐ€
45
+ `KnowledgeAssetError` ๋กœ ์ฃฝ์ธ๋‹ค โ€” run ์‹œ์ž‘ ์ „์—.
46
+ """
47
+ refs: dict[str, ResourceRef] = {}
48
+ for step in pipeline.steps:
49
+ for ref in step.resource_refs():
50
+ refs.setdefault(ref.label, ref)
51
+ hashes: dict[str, str | None] = {}
52
+ for label in sorted(refs):
53
+ path = refs[label].path
54
+ hashes[label] = content_sha256(path) if path is not None else None
55
+ return CollectedResources(hashes, {label: refs[label] for label in sorted(refs)})
56
+
57
+
58
+ def prompt_assets(prompt_path: Path) -> dict[str, str]:
59
+ """ํ”„๋กฌํ”„ํŠธ์˜ `{์ƒ๋Œ€๊ฒฝ๋กœ: sha256_64}` โ€” ๋””๋ ‰ํ† ๋ฆฌ๋ฉด legacy `prompt_sha` ์™€ ๊ฐ™์€ ํŒŒ์ผ ์ง‘ํ•ฉ.
60
+
61
+ ๋‹จ์ผ ํŒŒ์ผ(YAML ํ”„๋กฌํ”„ํŠธ)์€ `{"@file": sha256_64}`.
62
+
63
+ Raises:
64
+ FileNotFoundError: ๊ฒฝ๋กœ๊ฐ€ ์—†์„ ๋•Œ.
65
+ ValueError: ๋””๋ ‰ํ† ๋ฆฌ์— ์ง€๋ฌธ ๋Œ€์ƒ ํŒŒ์ผ์ด ํ•˜๋‚˜๋„ ์—†์„ ๋•Œ(๋นˆ ํ”„๋กฌํ”„ํŠธ๋กœ ๋ˆ run ์„ ๋ง‰๋Š”๋‹ค).
66
+ """
67
+ path = Path(prompt_path)
68
+ if path.is_dir():
69
+ assets = {
70
+ file.relative_to(path).as_posix(): content_sha256(file)
71
+ for file in iter_prompt_asset_files(path)
72
+ }
73
+ if not assets:
74
+ raise ValueError(f"prompt directory has no prompt asset files: {path}")
75
+ return assets
76
+ if path.is_file():
77
+ return {SINGLE_FILE_KEY: content_sha256(path)}
78
+ raise FileNotFoundError(f"prompt not found: {path}")
solar_eval/core/task_discovery.py CHANGED
@@ -15,7 +15,7 @@
15
 
16
  **๋ช…์‹œ ํƒœ์Šคํฌ๊ฐ€ ์ด๊ธด๋‹ค.** ์ด๋ฏธ `tasks:` ์— ์žˆ๋Š” dataset_path ๋Š” ๋ฐœ๊ฒฌ ๋Œ€์ƒ์—์„œ
17
  ๋น ์ง€๋ฏ€๋กœ, ๊ทœ์น™์— ์•ˆ ๋งž๋Š” ์ด๋ฆ„(`paragraph`, `yul-ryul`)์€ ์˜ˆ์ „ ๊ทธ๋Œ€๋กœ ์ ์–ด ๋‘๋ฉด
18
- ๋œ๋‹ค. ๊ณผ๊ฑฐ run ์ด ๊ทธ ์ด๋ฆ„์œผ๋กœ ๊ธฐ๋ก๋ผ ์žˆ์–ด ์ด๋ฆ„์„ ๋ฐ”๊พธ๋ฉด evalhub ์—์„œ ๊ณ„๋ณด๊ฐ€
19
  ๋Š์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์—, ์ด ์šฐ์„ ์ˆœ์œ„๋Š” ํƒ€ํ˜‘ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค.
20
  """
21
 
 
15
 
16
  **๋ช…์‹œ ํƒœ์Šคํฌ๊ฐ€ ์ด๊ธด๋‹ค.** ์ด๋ฏธ `tasks:` ์— ์žˆ๋Š” dataset_path ๋Š” ๋ฐœ๊ฒฌ ๋Œ€์ƒ์—์„œ
17
  ๋น ์ง€๋ฏ€๋กœ, ๊ทœ์น™์— ์•ˆ ๋งž๋Š” ์ด๋ฆ„(`paragraph`, `yul-ryul`)์€ ์˜ˆ์ „ ๊ทธ๋Œ€๋กœ ์ ์–ด ๋‘๋ฉด
18
+ ๋œ๋‹ค. ๊ณผ๊ฑฐ run ์ด ๊ทธ ์ด๋ฆ„์œผ๋กœ ๊ธฐ๋ก๋ผ ์žˆ์–ด ์ด๋ฆ„์„ ๋ฐ”๊พธ๋ฉด Experiment Hub ์—์„œ ๊ณ„๋ณด๊ฐ€
19
  ๋Š์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์—, ์ด ์šฐ์„ ์ˆœ์œ„๋Š” ํƒ€ํ˜‘ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค.
20
  """
21
 
solar_eval/evaluators/base.py CHANGED
@@ -19,15 +19,33 @@ class BaseEvaluator(ABC):
19
 
20
  required_fields: frozenset[str] = frozenset()
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  @abstractmethod
23
  async def evaluate(
24
  self,
25
  sample: EvalSample,
26
  provider: BaseProvider | None = None,
27
- judge_model: str = "gpt-4o",
28
  ) -> dict[str, Any]:
29
  """`sample` ํ•œ ๊ฑด์„ ์ฑ„์ ํ•œ๋‹ค. `{score, details}` ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
30
 
 
 
 
 
31
  ์ฑ„์ ์— ์‹คํŒจํ•˜๋ฉด(judge ํ˜ธ์ถœ ์˜ˆ์™ธ, ์ž˜๋ชป๋œ ์‘๋‹ต ๋“ฑ) ๊ทธ ์‹คํŒจ๋Š” **์˜ˆ์™ธ๋กœ ์ „ํŒŒํ•œ๋‹ค** --
32
  ์ ์ˆ˜(0.0 ์ด๋“  1.0 ์ด๋“ )๋กœ ์น˜ํ™˜ํ•ด ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š”๋‹ค. fail-open(์žฅ์• ๋ฅผ ๋งŒ์ ์œผ๋กœ
33
  ๋‘”๊ฐ‘์‹œํ‚ค๋Š” ๊ฒƒ)์„ ๋งŒ๋“ค์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ด ๊ณ„์•ฝ์˜ ํ•ต์‹ฌ์ด๋‹ค. ์ด ์˜ˆ์™ธ๋ฅผ ์žก์•„ ์‹คํŒจ
 
19
 
20
  required_fields: frozenset[str] = frozenset()
21
 
22
+ @property
23
+ def judge_model(self) -> str | None:
24
+ """์ฑ„์ ์— ์“ฐ๋Š” judge ๋ชจ๋ธ โ€” ๋ชจ๋ธ์„ ๋ถ€๋ฅด์ง€ ์•Š๋Š” ํ‰๊ฐ€๊ธฐ๋Š” None.
25
+
26
+ evaluation.json ์˜ `eval_model` ๊ณผ run.json ์Šค๋ƒ…์ƒท์˜ `evaluator_config.judge_model`
27
+ ์ด ์ด ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค(๊ณ„ํš 2026-09-24-phase1a D11). ์˜ˆ์ „์—๋Š” ๋Ÿฌ๋„ˆ๊ฐ€ ํ‰๊ฐ€๊ธฐ์™€
28
+ ๋ฌด๊ด€ํ•˜๊ฒŒ `"gpt-4o"` ๋ฅผ ์ ์–ด lcs_diff run ์—๋„ judge ๊ฐ€ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์˜€๋‹ค.
29
+ """
30
+ return None
31
+
32
+ def sub_evaluators(self) -> list["BaseEvaluator"]:
33
+ """ํ•ฉ์„ฑ ํ‰๊ฐ€๊ธฐ์˜ ํ•˜์œ„ ํ‰๊ฐ€๊ธฐ โ€” ์Šค๋ƒ…์ƒท์˜ ๊ตฌํ˜„ ํ•ด์‹œ๊ฐ€ ํ•˜์œ„ ๋ชจ๋“ˆ๊นŒ์ง€ ๋ณด๋„๋ก. ๊ธฐ๋ณธ []."""
34
+ return []
35
+
36
  @abstractmethod
37
  async def evaluate(
38
  self,
39
  sample: EvalSample,
40
  provider: BaseProvider | None = None,
41
+ judge_model: str | None = None,
42
  ) -> dict[str, Any]:
43
  """`sample` ํ•œ ๊ฑด์„ ์ฑ„์ ํ•œ๋‹ค. `{score, details}` ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
44
 
45
+ `judge_model` ์€ ํ˜ธ์ถœ์ž๊ฐ€ judge ๋ชจ๋ธ์„ **๋ฎ์–ด์“ธ** ๋•Œ๋งŒ ์ค€๋‹ค. None ์ด๋ฉด ํ‰๊ฐ€๊ธฐ๊ฐ€
46
+ ์ž๊ธฐ ์„ค์ •๊ฐ’(`self.judge_model`)์„ ์“ด๋‹ค โ€” ๋Ÿฌ๋„ˆยท`runs eval` ์€ ๋„˜๊ธฐ์ง€ ์•Š์œผ๋ฏ€๋กœ ๋ณด๊ณ ๊ฐ’๊ณผ
47
+ ์‹ค์ œ ํ˜ธ์ถœ ๋ชจ๋ธ์ด ์–ด๊ธ‹๋‚˜์ง€ ์•Š๋Š”๋‹ค.
48
+
49
  ์ฑ„์ ์— ์‹คํŒจํ•˜๋ฉด(judge ํ˜ธ์ถœ ์˜ˆ์™ธ, ์ž˜๋ชป๋œ ์‘๋‹ต ๋“ฑ) ๊ทธ ์‹คํŒจ๋Š” **์˜ˆ์™ธ๋กœ ์ „ํŒŒํ•œ๋‹ค** --
50
  ์ ์ˆ˜(0.0 ์ด๋“  1.0 ์ด๋“ )๋กœ ์น˜ํ™˜ํ•ด ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๋Š”๋‹ค. fail-open(์žฅ์• ๋ฅผ ๋งŒ์ ์œผ๋กœ
51
  ๋‘”๊ฐ‘์‹œํ‚ค๋Š” ๊ฒƒ)์„ ๋งŒ๋“ค์ง€ ์•Š๋Š” ๊ฒƒ์ด ์ด ๊ณ„์•ฝ์˜ ํ•ต์‹ฌ์ด๋‹ค. ์ด ์˜ˆ์™ธ๋ฅผ ์žก์•„ ์‹คํŒจ