"""rule 계열 스텝 -- `rules.py` 의 순수 함수를 레지스트리 스텝으로 감싼다. `multi_step.py`의 `_apply_rule` if/elif 디스패치에서 이동 (2026-08-10, 동작 동일). v24 파이프라인이 LLM 스텝 사이사이에 끼워 넣는 "code block 후처리"가 바로 이 스텝들이다. 각 rule_type 이 이제 독립 등록 항목이라, 새 후처리는 여기 클래스를 추가하는 게 아니라 어디서든 `@register_step` 으로 등록하면 된다. """ import logging from solar_eval.models.sample import EvalSample from solar_eval.pipelines.pipeline import BaseStep, StepContext from solar_eval.pipelines.registry import register_step from solar_eval.pipelines.rules import ( apply_change_rate_guard, apply_comma_removal, apply_compound_whitelist_restore, apply_correction_filter, apply_currency_compact, apply_judge_decisions, apply_pronoun_postprocess, apply_unit_unicode, apply_vocabulary, apply_yul_ryul_normalize, load_compound_whitelist, ) from solar_eval.pipelines.rules_josa import apply_josa_fix from solar_eval.pipelines.rules_revert import apply_diff_revert logger = logging.getLogger(__name__) @register_step("vocabulary_sub", "vocabulary_reapply") class VocabularyStep(BaseStep): """어휘 치환. CSV 는 run 수명 캐시(`ctx.shared`)에 한 번만 로드한다. 예전에는 `MultiStepPipeline._vocabulary` 단일 슬롯이라 서로 다른 CSV 를 선언한 두 vocab 스텝이 첫 로드를 잘못 공유할 수 있었다. 지금은 config (repo, 경로, source) 를 캐시 키로 써서 같은 CSV 만 공유한다 -- prod/v24 처럼 같은 CSV 를 두 번 쓰는 파이프라인은 여전히 1회 로드. """ async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) cache_key = ( "vocabulary", config.get("repo", ""), config.get("vocab_dataset", ""), config.get("source", "huggingface"), ) vocabulary = ctx.shared.get(cache_key) text, vocabulary = apply_vocabulary( ctx.text, config, vocabulary, self.env.dataset_loader, self.env.config_dir ) ctx.shared[cache_key] = vocabulary ctx.text = text @register_step("post_process") class PostProcessStep(BaseStep): """결정론적 후처리 묶음 -- config 플래그로 켜는 대명사/통화/쉼표/단위 치환.""" async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) text = ctx.text if config.get("pronoun_replacements"): text = apply_pronoun_postprocess(text, config["pronoun_replacements"]) if config.get("currency_compact"): text = apply_currency_compact(text) if config.get("comma_removal"): text = apply_comma_removal(text) if config.get("unit_unicode"): text = apply_unit_unicode(text) ctx.text = text @register_step("compound_whitelist_restore") class CompoundWhitelistRestoreStep(BaseStep): """파이프라인이 갈라놓은 화이트리스트 복합어를 원문 표기로 되돌린다 (v19/v32 계보). 화이트리스트는 run 수명 캐시(`ctx.shared`)에 한 번만 로드한다 — 경로가 다른 두 스텝이 캐시를 잘못 공유하지 않도록 경로를 캐시 키에 넣는다. """ async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) cache_key = ("compound_whitelist", config.get("whitelist_path", "")) whitelist = ctx.shared.get(cache_key) if whitelist is None: whitelist = load_compound_whitelist(config, self.env.config_dir) ctx.shared[cache_key] = whitelist ctx.text = apply_compound_whitelist_restore(ctx.text, ctx.original_text, whitelist) @register_step("yul_ryul_normalize") class YulRyulNormalizeStep(BaseStep): """율/률 받침 규칙(맞춤법 제11항)을 결정론적으로 강제한다. 조사 가드 + `exceptions`(인명 등) 2중 방어로 접미사 용법일 때만 고친다 — 상세와 재생 실측은 `rules.apply_yul_ryul_normalize` docstring 참조. """ async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) exceptions = tuple(config.get("exceptions") or ()) ctx.text = apply_yul_ryul_normalize(ctx.text, exceptions) @register_step("correction_filter") class CorrectionFilterStep(BaseStep): async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) ctx.text = apply_correction_filter(ctx.text, config, ctx.original_text) @register_step("apply_judge_decisions") class ApplyJudgeDecisionsStep(BaseStep): """`tool_calling_judge` 가 `sample.artifacts` 에 남긴 구조화 결정을 텍스트에 적용.""" async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) ctx.text = apply_judge_decisions(ctx.text, config, sample) @register_step("josa_fix") class JosaFixStep(BaseStep): """기호·숫자·단위 뒤 조사 결정론 교정 (2026-09-18, 상세는 `rules_josa` docstring).""" async def run(self, sample: EvalSample, ctx: StepContext) -> None: ctx.text = apply_josa_fix(ctx.text) @register_step("diff_revert") class DiffRevertStep(BaseStep): """원문 대비 diff 를 클래스 판정해 되돌리는 정밀도 가드 (2026-09-18). 글리프 변환·쉼표 삽입·금지 치환·격식 전환만 되돌린다 — 상세와 실측 근거는 `rules_revert` docstring 참조. 파이프라인 꼬리(post_process 뒤)에 두는 것을 전제로 설계했다. """ async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) ctx.text = apply_diff_revert(ctx.text, ctx.original_text, config) @register_step("change_rate_guard") class ChangeRateGuardStep(BaseStep): """원문 대비 변경률이 임계를 넘으면 원문으로 되돌리는 안전망 (v17 계열).""" async def run(self, sample: EvalSample, ctx: StepContext) -> None: config = self.config.get("config", {}) ctx.text = apply_change_rate_guard(ctx.text, ctx.original_text, config)