""" Infospace runtime state. Computed from the current entities, evaluations, and metrics on disk. Provides the data behind ``markitect infospace status`` and ``markitect infospace viability``. """ from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional from markitect.infospace.config import InfospaceConfig, ViabilityThreshold from markitect.infospace.models import EntityMeta from markitect.infospace.evaluation import EvaluationSnapshot @dataclass class ViabilityResult: """Result of checking a single viability threshold.""" metric: str value: float threshold: ViabilityThreshold passed: bool def to_dict(self) -> Dict[str, Any]: d: Dict[str, Any] = { "metric": self.metric, "value": self.value, "passed": self.passed, } if self.threshold.min is not None: d["min"] = self.threshold.min if self.threshold.max is not None: d["max"] = self.threshold.max return d @dataclass class InfospaceState: """Current runtime state of an infospace. Aggregates entity metadata, evaluation results, and viability checks into a single queryable object. """ config: InfospaceConfig entities: List[EntityMeta] = field(default_factory=list) latest_snapshot: Optional[EvaluationSnapshot] = None viability_results: List[ViabilityResult] = field(default_factory=list) computed_at: datetime = field(default_factory=datetime.utcnow) @property def entity_count(self) -> int: return len(self.entities) @property def topic_name(self) -> str: return self.config.topic.name @property def is_viable(self) -> bool: """``True`` if all viability thresholds are met.""" if not self.viability_results: return False return all(r.passed for r in self.viability_results) @property def viability_pass_count(self) -> int: return sum(1 for r in self.viability_results if r.passed) @property def viability_total_count(self) -> int: return len(self.viability_results) @property def domains(self) -> List[str]: """Distinct domain values across all entities.""" return sorted({e.domain for e in self.entities if e.domain}) @property def has_evaluations(self) -> bool: return self.latest_snapshot is not None def check_viability(self, metrics: Dict[str, float]) -> List[ViabilityResult]: """Check *metrics* against the configured viability thresholds. Updates :attr:`viability_results` and returns the results. """ results: List[ViabilityResult] = [] for name, threshold in self.config.viability.items(): value = metrics.get(name, 0.0) results.append(ViabilityResult( metric=name, value=value, threshold=threshold, passed=threshold.check(value), )) self.viability_results = results return results def summary(self) -> Dict[str, Any]: """Return a summary dict suitable for display or serialisation.""" d: Dict[str, Any] = { "topic": self.topic_name, "entity_count": self.entity_count, "domains": self.domains, "has_evaluations": self.has_evaluations, } if self.viability_results: d["viable"] = self.is_viable d["viability_pass"] = self.viability_pass_count d["viability_total"] = self.viability_total_count if self.latest_snapshot: d["last_evaluated"] = self.latest_snapshot.created_at.isoformat() return d def build_state( config: InfospaceConfig, entities: Optional[List[EntityMeta]] = None, snapshot: Optional[EvaluationSnapshot] = None, metrics: Optional[Dict[str, float]] = None, ) -> InfospaceState: """Build an :class:`InfospaceState` from available data. This is a convenience function that assembles the state object and optionally runs viability checks if *metrics* are provided. """ state = InfospaceState( config=config, entities=entities or [], latest_snapshot=snapshot, ) if metrics is not None: state.check_viability(metrics) return state