Files
markitect-main/markitect/infospace/state.py
tegwick b20fe4db68 feat(infospace): add infospace configuration model and state (S2.1)
InfospaceConfig (topic, disciplines, schemas, competency questions,
viability thresholds, pipeline) with YAML load/save and directory
discovery. InfospaceState aggregates entities, evaluations, and
viability checks for status reporting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 01:44:14 +01:00

142 lines
4.3 KiB
Python

"""
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