All checks were successful
Deploy Development / deploy (push) Successful in 46s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m18s
- Implemented `_build_evaluate_empty_slot_gap_specs` function to generate gap offer specifications for unfilled roadmap slots in evaluate-only mode. - Enhanced `ProgressionFindingsPanel` to display AI offers for empty slots and gaps, improving user interaction and clarity. - Updated `ProgressionGraphEditor` and `ProgressionSlotCard` components to support new functionalities for managing slots and offers. - Refactored utility functions in `progressionGraphDraft.js` to streamline slot management and offer handling. - Incremented application version to reflect these updates.
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""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
|
|
|
|
@field_validator("progression_roadmap", "path_skill_expectations", "last_findings", 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",
|
|
]
|