Files
rpa_vision_v3/core/cognition/scene_expected.py
Dom 7bb8d543ab feat(cognition): dataclasses Trace + SceneExpected + Precondition (Phase 2.1)
Crée les 3 dataclasses du modèle Mandat/Protocoles/Scènes v0.3 dans
core/cognition/, standalone (aucun branchement runtime), avec
sérialisation JSON explicite et tests offline.

Préparation des phases :
- Phase 2.1 plan : objet Trace (mandate_id, intention_id, scene_id,
  affordance_signature, expected_retour, level_of_delegation)
- Workpack A : SceneExpected (monitor_index, app_name, title_patterns,
  title_anti, window_rect_hint, scene_role, accepted_transitions,
  stability_ms) + helper matches_title()
- Workpack B : Precondition (kind, window_title_must_contain/anti,
  critic_question, verify_timeout_ms) + PreconditionRecovery
  (max_attempts, on_recovery_fail, actions)

Toutes les dataclasses sont frozen, immutables, avec to_dict/from_dict
tolérants (champs vides/None -> instance vide). Validation au __post_init__
pour Precondition.kind et PreconditionRecovery.on_recovery_fail.

Aucune dépendance runtime obligatoire : si l'objet n'est pas posé sur
une action, fallback comportement actuel. Aucune modif executor /
api_stream / replay_engine / grounding.

Tests : 22/22 passent (sérialisation JSON, contrats from_dict tolérants,
validation kinds, helpers matches_title/check_title, anti-intention).

Tag rollback : rollback/pre-cognition-dataclasses-2026-05-25_0610
2026-05-25 06:08:18 +02:00

101 lines
4.3 KiB
Python

"""Scène d'intention attendue — workpack A attention scope multi-écrans.
Cf. docs/coordination/inbox_codex/2026-05-25_0610_claude-to-codex_workpack-A-attention-scope-multi-ecrans.md
"""
from __future__ import annotations
from dataclasses import dataclass, field, asdict
from typing import Any, Dict, List, Optional, Tuple
@dataclass(frozen=True)
class SceneExpected:
"""Description du périmètre visuel attendu pour servir l'intention.
Construit au build serveur, transporté additif jusqu'au client, consommé
par une garde `_assert_scene_active()` avant tout geste — surtout les
raccourcis clavier qui partent sinon dans la fenêtre active globale.
Attributs
scene_id : ID stable de la scène
app_name : nom de l'application attendue (ex 'Notepad')
title_patterns : patterns de titre acceptables (substrings)
title_anti : patterns de titre interdits (anti-intention)
monitor_index : index du moniteur (1-based mss). None = quelconque
monitor_geometry : (left, top, width, height) en pixels. Optionnel.
window_rect_hint : (left, top, right, bottom) zone attendue. Optionnel.
scene_role : 'editor' | 'dialog' | 'menu' | 'browser_tab' | ...
required : True si le geste DOIT être bloqué si scène absente
stability_ms : durée min de stabilité avant le geste
accepted_transitions: scènes vers lesquelles transition est attendue
"""
scene_id: str = ""
app_name: str = ""
title_patterns: Tuple[str, ...] = field(default_factory=tuple)
title_anti: Tuple[str, ...] = field(default_factory=tuple)
monitor_index: Optional[int] = None
monitor_geometry: Optional[Tuple[int, int, int, int]] = None
window_rect_hint: Optional[Tuple[int, int, int, int]] = None
scene_role: str = ""
required: bool = True
stability_ms: int = 0
accepted_transitions: Tuple[str, ...] = field(default_factory=tuple)
def to_dict(self) -> Dict[str, Any]:
d = asdict(self)
d["title_patterns"] = list(self.title_patterns)
d["title_anti"] = list(self.title_anti)
d["accepted_transitions"] = list(self.accepted_transitions)
if self.monitor_geometry is not None:
d["monitor_geometry"] = list(self.monitor_geometry)
if self.window_rect_hint is not None:
d["window_rect_hint"] = list(self.window_rect_hint)
return d
@classmethod
def from_dict(cls, data: Optional[Dict[str, Any]]) -> "SceneExpected":
if not data:
return cls()
def _tuple_of_4(v):
if v is None:
return None
try:
lst = list(v)
if len(lst) != 4:
return None
return tuple(int(x) for x in lst)
except (TypeError, ValueError):
return None
return cls(
scene_id=str(data.get("scene_id", "") or ""),
app_name=str(data.get("app_name", "") or ""),
title_patterns=tuple(str(x) for x in (data.get("title_patterns") or [])),
title_anti=tuple(str(x) for x in (data.get("title_anti") or [])),
monitor_index=(int(data["monitor_index"]) if data.get("monitor_index") is not None else None),
monitor_geometry=_tuple_of_4(data.get("monitor_geometry")),
window_rect_hint=_tuple_of_4(data.get("window_rect_hint")),
scene_role=str(data.get("scene_role", "") or ""),
required=bool(data.get("required", True)),
stability_ms=int(data.get("stability_ms", 0) or 0),
accepted_transitions=tuple(str(x) for x in (data.get("accepted_transitions") or [])),
)
def matches_title(self, observed_title: str) -> bool:
"""Vrai si le titre observé est cohérent avec la scène (patterns + anti)."""
if not observed_title:
return False
norm = observed_title.lower()
for anti in self.title_anti:
if anti and anti.lower() in norm:
return False
if not self.title_patterns:
return True
return any(p and p.lower() in norm for p in self.title_patterns)
def is_empty(self) -> bool:
return not (self.scene_id or self.app_name or self.title_patterns)