| from __future__ import annotations |
|
|
| from dataclasses import asdict, dataclass |
| from datetime import date |
| from typing import Any |
| from normative_status import NormativeStatus |
|
|
|
|
| DecisionStatus = NormativeStatus |
|
|
|
|
| @dataclass(frozen=True) |
| class DecisionResult: |
| status: str |
| decision_type: str |
| outcome: Any = None |
| reason: str = "" |
| contract_ids: tuple[str, ...] = () |
| matched_rule_ids: tuple[str, ...] = () |
| source_refs: tuple[str, ...] = () |
| missing_facts: tuple[str, ...] = () |
| judgment_requirements: tuple[str, ...] = () |
| trace: tuple[dict[str, Any], ...] = () |
|
|
| def to_dict(self) -> dict[str, Any]: |
| return asdict(self) |
|
|
|
|
| class NormativeDecisionRuntime: |
| """Deterministic evaluator for expert-approved MCKF decision contracts. |
| |
| Natural language, embeddings and LLM output never enter this evaluator. |
| The same package version and structured facts always produce the same |
| result and proof trace. |
| """ |
|
|
| def __init__(self, contracts: list[dict[str, Any]] | None = None, build_id: str = "") -> None: |
| self.contracts = [dict(item) for item in (contracts or [])] |
| self.build_id = build_id |
|
|
| @classmethod |
| def from_corpus(cls, corpus: dict[str, Any] | None) -> "NormativeDecisionRuntime": |
| import json |
| from pathlib import Path |
| corpus = corpus or {} |
| contracts = list(corpus.get("decision_contracts", []) or []) |
| registry_path = Path(__file__).parent / "data" / "registry" / "normative_decision_contracts.json" |
| if registry_path.exists(): |
| try: |
| registry_data = json.loads(registry_path.read_text(encoding="utf-8")) |
| registry_contracts = registry_data.get("contracts", []) or [] |
| existing_ids = {c.get("contract_id") for c in contracts if c.get("contract_id")} |
| for rc in registry_contracts: |
| if rc.get("contract_id") not in existing_ids: |
| contracts.append(rc) |
| existing_ids.add(rc.get("contract_id")) |
| except Exception: |
| pass |
| return cls(contracts, str(corpus.get("build_id", "") or "")) |
|
|
|
|
| def evaluate( |
| self, |
| decision_type: str, |
| facts: dict[str, Any] | None, |
| as_of_date: str = "", |
| ) -> dict[str, Any]: |
| decision_type = str(decision_type or "").strip() |
| facts = facts if isinstance(facts, dict) else {} |
| candidates = [ |
| item for item in self.contracts |
| if str(item.get("decision_type", "") or "") == decision_type |
| and _is_effective(item, as_of_date) |
| ] |
| if not candidates: |
| return DecisionResult( |
| status=DecisionStatus.OUT_OF_SCOPE.value, |
| decision_type=decision_type, |
| reason="Yayınlanmış MCKF paketinde bu karar türü ve tarih için çalıştırılabilir sözleşme yok.", |
| ).to_dict() |
|
|
| evaluations = [_evaluate_contract(contract, facts, decision_type) for contract in candidates] |
| decided = [item for item in evaluations if item.status == DecisionStatus.DECIDED.value] |
| distinct_outcomes = {_stable_value(item.outcome) for item in decided} |
| if len(distinct_outcomes) > 1: |
| return DecisionResult( |
| status=DecisionStatus.CONFLICT.value, |
| decision_type=decision_type, |
| reason="Aynı olay verileri yürürlükteki karar sözleşmelerinde farklı sonuçlar üretti.", |
| contract_ids=tuple(item.contract_ids[0] for item in decided), |
| matched_rule_ids=tuple(rule for item in decided for rule in item.matched_rule_ids), |
| source_refs=tuple(dict.fromkeys(ref for item in decided for ref in item.source_refs)), |
| trace=tuple(step for item in evaluations for step in item.trace), |
| ).to_dict() |
| if decided: |
| winner = decided[0] |
| return winner.to_dict() |
|
|
| for status in ( |
| DecisionStatus.REQUIRES_JUDGMENT.value, |
| DecisionStatus.UNKNOWN.value, |
| DecisionStatus.CONFLICT.value, |
| ): |
| matching = [item for item in evaluations if item.status == status] |
| if matching: |
| merged = matching[0] |
| return DecisionResult( |
| status=status, |
| decision_type=decision_type, |
| reason=merged.reason, |
| contract_ids=tuple(item.contract_ids[0] for item in matching), |
| source_refs=tuple(dict.fromkeys(ref for item in matching for ref in item.source_refs)), |
| missing_facts=tuple(dict.fromkeys(fact for item in matching for fact in item.missing_facts)), |
| judgment_requirements=tuple( |
| dict.fromkeys(req for item in matching for req in item.judgment_requirements) |
| ), |
| trace=tuple(step for item in evaluations for step in item.trace), |
| ).to_dict() |
| return DecisionResult( |
| status=DecisionStatus.OUT_OF_SCOPE.value, |
| decision_type=decision_type, |
| reason="Sözleşme kapsamı bulundu ancak verilen olay için uygulanabilir karar kolu yok.", |
| contract_ids=tuple(str(item.get("contract_id", "")) for item in candidates), |
| ).to_dict() |
|
|
| def governance_report(self) -> dict[str, Any]: |
| warnings = [] |
| for contract in self.contracts: |
| contract_id = str(contract.get("contract_id", "") or "") |
| for requirement in contract.get("judgment_requirements", []) or []: |
| warnings.append({ |
| "status": DecisionStatus.REQUIRES_JUDGMENT.value, |
| "contract_id": contract_id, |
| "fact": requirement.get("fact", ""), |
| "message": requirement.get("reason", "İnsan değerlendirmesi gereken açık normatif kavram."), |
| "source_refs": _source_refs(contract), |
| }) |
| conflicts = _static_conflicts(contract) |
| warnings.extend(conflicts) |
| if not contract.get("rules"): |
| warnings.append({ |
| "status": DecisionStatus.OUT_OF_SCOPE.value, |
| "contract_id": contract_id, |
| "message": "Karar sözleşmesinde çalıştırılabilir kural bulunmuyor.", |
| "source_refs": _source_refs(contract), |
| }) |
| return { |
| "schema": "MCKF-DecisionGovernanceReport-v1.0", |
| "build_id": self.build_id, |
| "contract_count": len(self.contracts), |
| "warning_count": len(warnings), |
| "warnings": warnings, |
| } |
|
|
|
|
| def _evaluate_contract(contract: dict[str, Any], facts: dict[str, Any], decision_type: str) -> DecisionResult: |
| contract_id = str(contract.get("contract_id", "") or "") |
| source_refs = tuple(_source_refs(contract)) |
| schema_fields = (contract.get("input_schema", {}) or {}).get("fields", {}) or {} |
| required = [name for name, spec in schema_fields.items() if (spec or {}).get("required")] |
| judgment_requirements = contract.get("judgment_requirements", []) or [] |
| unresolved_judgment = [ |
| item for item in judgment_requirements |
| if _fact_value(facts, str(item.get("fact", "") or ""), _MISSING) is _MISSING |
| ] |
| if unresolved_judgment: |
| return DecisionResult( |
| status=DecisionStatus.REQUIRES_JUDGMENT.value, |
| decision_type=decision_type, |
| reason="Hukukun açık bıraktığı değerlendirme alanı insan kararı gerektiriyor.", |
| contract_ids=(contract_id,), |
| source_refs=source_refs, |
| judgment_requirements=tuple( |
| str(item.get("reason", "") or item.get("fact", "")) for item in unresolved_judgment |
| ), |
| ) |
|
|
| missing = [name for name in required if _fact_value(facts, name, _MISSING) is _MISSING] |
| if missing: |
| return DecisionResult( |
| status=DecisionStatus.UNKNOWN.value, |
| decision_type=decision_type, |
| reason="Deterministik karar için zorunlu olay verileri eksik.", |
| contract_ids=(contract_id,), |
| source_refs=source_refs, |
| missing_facts=tuple(missing), |
| ) |
|
|
| matched = [] |
| trace = [] |
| for rule in contract.get("rules", []) or []: |
| conditions = rule.get("when", {}) or {} |
| applies, condition_trace = _conditions_match(conditions, facts) |
| trace.append({ |
| "contract_id": contract_id, |
| "rule_id": rule.get("rule_id", ""), |
| "applies": applies, |
| "conditions": condition_trace, |
| }) |
| if applies: |
| matched.append(rule) |
| if not matched and "default_outcome" in contract: |
| return DecisionResult( |
| status=DecisionStatus.DECIDED.value, |
| decision_type=decision_type, |
| outcome=contract.get("default_outcome"), |
| reason="Hiçbir özel kural eşleşmedi; uzman onaylı varsayılan sonuç uygulandı.", |
| contract_ids=(contract_id,), |
| source_refs=source_refs, |
| trace=tuple(trace), |
| ) |
| if not matched: |
| return DecisionResult( |
| status=DecisionStatus.OUT_OF_SCOPE.value, |
| decision_type=decision_type, |
| reason="Girdiler tam olsa da bu olay için sözleşmede uygulanabilir kural bulunmuyor.", |
| contract_ids=(contract_id,), |
| source_refs=source_refs, |
| trace=tuple(trace), |
| ) |
|
|
| highest = max(int(item.get("priority", 0) or 0) for item in matched) |
| winners = [item for item in matched if int(item.get("priority", 0) or 0) == highest] |
| outcomes = {_stable_value(item.get("outcome")) for item in winners} |
| if len(outcomes) > 1: |
| return DecisionResult( |
| status=DecisionStatus.CONFLICT.value, |
| decision_type=decision_type, |
| reason="Aynı öncelikte birden fazla kural farklı sonuç üretti.", |
| contract_ids=(contract_id,), |
| matched_rule_ids=tuple(str(item.get("rule_id", "")) for item in winners), |
| source_refs=source_refs, |
| trace=tuple(trace), |
| ) |
| winner = sorted(winners, key=lambda item: str(item.get("rule_id", "")))[0] |
| return DecisionResult( |
| status=DecisionStatus.DECIDED.value, |
| decision_type=decision_type, |
| outcome=winner.get("outcome"), |
| reason=str(winner.get("explanation", "") or "Uzman onaylı normatif kural uygulandı."), |
| contract_ids=(contract_id,), |
| matched_rule_ids=(str(winner.get("rule_id", "")),), |
| source_refs=source_refs, |
| trace=tuple(trace), |
| ) |
|
|
|
|
| def _conditions_match(conditions: dict[str, Any], facts: dict[str, Any]) -> tuple[bool, list[dict[str, Any]]]: |
| mode = "any" if "any" in conditions else "all" |
| rows = conditions.get(mode, []) or [] |
| if not rows: |
| return True, [] |
| trace = [] |
| results = [] |
| for condition in rows: |
| fact = str(condition.get("fact", "") or "") |
| actual = _fact_value(facts, fact, _MISSING) |
| operator = str(condition.get("operator", "eq") or "eq") |
| expected = condition.get("value") |
| result = False if actual is _MISSING else _compare(actual, operator, expected) |
| results.append(result) |
| trace.append({"fact": fact, "operator": operator, "expected": expected, "actual": None if actual is _MISSING else actual, "result": result}) |
| return (any(results) if mode == "any" else all(results)), trace |
|
|
|
|
| def _compare(actual: Any, operator: str, expected: Any) -> bool: |
| operations = { |
| "eq": lambda: actual == expected, |
| "ne": lambda: actual != expected, |
| "gt": lambda: actual > expected, |
| "gte": lambda: actual >= expected, |
| "lt": lambda: actual < expected, |
| "lte": lambda: actual <= expected, |
| "in": lambda: actual in expected, |
| "not_in": lambda: actual not in expected, |
| "truthy": lambda: bool(actual), |
| "falsy": lambda: not bool(actual), |
| } |
| try: |
| return bool(operations[operator]()) |
| except (KeyError, TypeError, ValueError): |
| return False |
|
|
|
|
| def _fact_value(facts: dict[str, Any], path: str, default: Any) -> Any: |
| value: Any = facts |
| for part in path.split("."): |
| if not isinstance(value, dict) or part not in value: |
| return default |
| value = value[part] |
| return value |
|
|
|
|
| def _source_refs(contract: dict[str, Any]) -> list[str]: |
| return [ |
| f"{item.get('document_id', '')}::{item.get('article_id', '')}" |
| for item in contract.get("source_refs", []) or [] |
| if item.get("document_id") and item.get("article_id") |
| ] |
|
|
|
|
| def _is_effective(contract: dict[str, Any], value: str) -> bool: |
| if not value: |
| return True |
| try: |
| target = date.fromisoformat(value) |
| start = date.fromisoformat(contract["effective_from"]) if contract.get("effective_from") else None |
| end = date.fromisoformat(contract["effective_to"]) if contract.get("effective_to") else None |
| except (TypeError, ValueError): |
| return False |
| return (not start or target >= start) and (not end or target <= end) |
|
|
|
|
| def _static_conflicts(contract: dict[str, Any]) -> list[dict[str, Any]]: |
| signatures: dict[str, dict[str, Any]] = {} |
| warnings = [] |
| for rule in contract.get("rules", []) or []: |
| signature = _stable_value({"priority": rule.get("priority", 0), "when": rule.get("when", {})}) |
| previous = signatures.get(signature) |
| if previous and _stable_value(previous.get("outcome")) != _stable_value(rule.get("outcome")): |
| warnings.append({ |
| "status": DecisionStatus.CONFLICT.value, |
| "contract_id": contract.get("contract_id", ""), |
| "rule_ids": [previous.get("rule_id", ""), rule.get("rule_id", "")], |
| "message": "Aynı koşul ve öncelik için farklı sonuçlar tanımlanmış.", |
| "source_refs": _source_refs(contract), |
| }) |
| signatures[signature] = rule |
| return warnings |
|
|
|
|
| def _stable_value(value: Any) -> str: |
| import json |
|
|
| return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) |
|
|
|
|
| _MISSING = object() |
|
|