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>
This commit is contained in:
141
markitect/infospace/state.py
Normal file
141
markitect/infospace/state.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user