"""Trace causale d'une action — modèle Mandat/Protocoles/Scènes v0.3. Cf. docs/architecture/MODELE_MANDAT_PROTOCOLS_LEA_2026-05-25_v0.3_ARBITRAGES_DOM.md """ from __future__ import annotations from dataclasses import dataclass, field, asdict from typing import Any, Dict, Optional @dataclass(frozen=True) class Trace: """Contrat unificateur transporté du build au runtime à la preuve. Tous les champs sont optionnels (str vide / None) pour permettre une introduction progressive sans casser les actions existantes qui n'en portent pas. Fallback : comportement actuel si trace absente. Attributs mandate_id : ID du mandat humain de niveau supérieur intention_id : ID du sous-but courant servant le mandat scene_id : ID de la scène d'intention pertinente affordance_signature: signature stable de l'affordance ciblée expected_retour : description courte du retour attendu level_of_delegation : N0..N4 (cf v0.3 arbitrage 3) """ mandate_id: str = "" intention_id: str = "" scene_id: str = "" affordance_signature: str = "" expected_retour: str = "" level_of_delegation: int = 0 def to_dict(self) -> Dict[str, Any]: return asdict(self) @classmethod def from_dict(cls, data: Optional[Dict[str, Any]]) -> "Trace": if not data: return cls() return cls( mandate_id=str(data.get("mandate_id", "") or ""), intention_id=str(data.get("intention_id", "") or ""), scene_id=str(data.get("scene_id", "") or ""), affordance_signature=str(data.get("affordance_signature", "") or ""), expected_retour=str(data.get("expected_retour", "") or ""), level_of_delegation=int(data.get("level_of_delegation", 0) or 0), ) def is_empty(self) -> bool: return not ( self.mandate_id or self.intention_id or self.scene_id or self.affordance_signature or self.expected_retour )