"""Validierung und Normalisierung des Planungs-Artefakts am Progressionsgraph.""" from __future__ import annotations import json from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field, field_validator ARTIFACT_SCHEMA_VERSION = 1 _MAX_JSON_BYTES = 64_000 class SlotExerciseContent(BaseModel): kind: str = Field(default="empty", pattern=r"^(empty|library|proposal)$") exercise_id: Optional[int] = Field(default=None, ge=1) variant_id: Optional[int] = Field(default=None, ge=1) title: Optional[str] = Field(default=None, max_length=500) variant_name: Optional[str] = Field(default=None, max_length=200) proposal_key: Optional[str] = Field(default=None, max_length=120) ai_suggestion: Optional[Dict[str, Any]] = None class SlotContentEntry(BaseModel): major_step_index: int = Field(ge=0, le=20) primary: SlotExerciseContent = Field(default_factory=SlotExerciseContent) siblings: List[SlotExerciseContent] = Field(default_factory=list) class GraphPlanningRoadmapArtifact(BaseModel): schema_version: int = Field(default=ARTIFACT_SCHEMA_VERSION, ge=1, le=1) goal_query: str = Field(default="", max_length=2000) start_situation: Optional[str] = Field(default=None, max_length=2000) target_state: Optional[str] = Field(default=None, max_length=2000) roadmap_notes: Optional[str] = Field(default=None, max_length=2000) max_steps: int = Field(default=5, ge=2, le=10) progression_roadmap: Optional[Dict[str, Any]] = None path_skill_expectations: Optional[Dict[str, Any]] = None slot_contents: Optional[List[SlotContentEntry]] = None last_findings: Optional[Dict[str, Any]] = None planning_catalog_context: Optional[Dict[str, Any]] = None @field_validator("progression_roadmap", "path_skill_expectations", "last_findings", "planning_catalog_context", mode="before") @classmethod def _empty_dict_to_none(cls, v): if v == {}: return None return v @field_validator("slot_contents", mode="before") @classmethod def _empty_slot_list_to_none(cls, v): if v == []: return None return v def normalize_planning_roadmap_payload(raw: Any) -> Optional[Dict[str, Any]]: """None erlaubt (löschen); sonst validiertes Dict.""" if raw is None: return None if not isinstance(raw, dict): raise ValueError("planning_roadmap muss ein JSON-Objekt sein") artifact = GraphPlanningRoadmapArtifact.model_validate(raw) out = artifact.model_dump(exclude_none=True) blob = json.dumps(out, ensure_ascii=False) if len(blob.encode("utf-8")) > _MAX_JSON_BYTES: raise ValueError("planning_roadmap ist zu groß (max. 64 KB)") return out __all__ = [ "ARTIFACT_SCHEMA_VERSION", "GraphPlanningRoadmapArtifact", "SlotContentEntry", "SlotExerciseContent", "normalize_planning_roadmap_payload", ]