Ctrl K
ComunidadGuías…

src/rules/loader.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Unified V1/V2 rule bundle loading entry point."""

from __future__ import annotations

import json
import copy
from pathlib import Path
from typing import Any

from src.compat.rules_v1 import load_v1_template
from src.content.rule_locale import materialize_rule
from src.engine.currency import validate_declared_currency_system
from src.engine.language import content_locale_candidates


class RuleBundleLoader:
    """Load a V1 JSON rule or a V2 bundle without changing RuleSystem behavior."""

    def load(self, path: str | Path) -> dict[str, Any]:
        source = Path(path)
        with source.open(encoding="utf-8") as handle:
            raw = json.load(handle)
        if not isinstance(raw, dict):
            raise ValueError("rule bundle must be a JSON object")
        if raw.get("locale_schema_version") and raw.get("target"):
            target = raw.get("target") or {}
            core = source.parents[2] / f"{target.get('id')}.json"
            return materialize_rule(self.load(core), raw)
        if int(raw.get("rule_schema_version", raw.get("content_schema_version", 1)) or 1) >= 2:
            if not str(raw.get("rule_id") or raw.get("id") or "").strip():
                raise ValueError("V2 rule bundle requires rule_id")
            template = self._resolve_v2(source, raw)
        else:
            template = load_v1_template(source)
        # 显式声明版本的货币 schema 在加载边界 fail-fast(非法/未支持版本);
        # 无版本键的旧规则与显式 legacy(schema_version: 1)继续按 legacy 处理。
        validate_declared_currency_system(template.get("currency_system"))
        return template

    def _resolve_v2(self, source: Path, raw: dict[str, Any], seen: set[Path] | None = None) -> dict[str, Any]:
        parent = raw.get("extends")
        if not parent:
            return raw
        seen = seen or set()
        source = source.resolve()
        if source in seen:
            raise ValueError("V2 rule inheritance cycle")
        seen.add(source)
        parent_path = Path(str(parent))
        if not parent_path.suffix:
            parent_path = parent_path.with_suffix(".json")
        if not parent_path.is_absolute():
            parent_path = source.parent / parent_path
        if not parent_path.exists():
            builtin = source.parents[2] / parent_path.name
            parent_path = builtin if builtin.exists() else parent_path
        parent_raw = json.loads(parent_path.read_text(encoding="utf-8"))
        parent_resolved = self._resolve_v2(parent_path, parent_raw, seen)
        return self._merge_core(parent_resolved, raw)

    @classmethod
    def _merge_core(cls, base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
        result = copy.deepcopy(base)
        for key, value in override.items():
            if isinstance(result.get(key), dict) and isinstance(value, dict):
                result[key] = cls._merge_core(result[key], value)
            else:
                result[key] = copy.deepcopy(value)
        return result

    def load_rule(self, rules_dir: str | Path, rule_id: str, locale: str = "") -> dict[str, Any]:
        root = Path(rules_dir)
        core = root / f"{rule_id}.json"
        requested = str(locale or "").replace("_", "-")
        if not requested and core.exists():
            try:
                requested = str(json.loads(core.read_text(encoding="utf-8")).get("default_locale") or "")
            except (OSError, ValueError):
                requested = ""
        if requested:
            for candidate in content_locale_candidates(requested):
                localized = root / "locales" / candidate / f"{rule_id}.json"
                if localized.exists():
                    return self.load(localized)
        return self.load(core)

    def load_system(self, path: str | Path, locale: str = "") -> Any:
        from src.rules.rule_system import RuleSystem
        source = Path(path)
        if locale and source.parent.name == "rules":
            return RuleSystem(self.load_rule(source.parent, source.stem, locale))
        return RuleSystem(self.load(path))