- `markitect infospace entity <name>`: single-entity lookup tolerating hyphens/underscores/case, with substring matching, ambiguity listing, and near-match hints. Prints slug, source path, domain, chapter, word count, VSM system, overall score, evaluator, and evaluation file path. - `markitect infospace evaluate --model-fallback <model>`: if any entities fail with a rate-limit error, retry just those with a fresh adapter on the fallback model (different free-tier models have separate quota buckets). - `markitect llm-check`: advisory when `OPENROUTER_API_KEY` is set but not used by the resolved provider; targeted hint when OpenRouter returns 401 (almost always a stale env key). - `build_state`: raises `TypeError` with actionable message if passed a path instead of an `InfospaceConfig` — prior failure mode was a confusing `AttributeError` deep in the stack. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
148 lines
4.6 KiB
Python
148 lines
4.6 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.
|
|
"""
|
|
if not isinstance(config, InfospaceConfig):
|
|
raise TypeError(
|
|
f"build_state(config=...) expects an InfospaceConfig instance, "
|
|
f"got {type(config).__name__}. If you have a path, load the "
|
|
f"config first with load_infospace_config(path)."
|
|
)
|
|
state = InfospaceState(
|
|
config=config,
|
|
entities=entities or [],
|
|
latest_snapshot=snapshot,
|
|
)
|
|
if metrics is not None:
|
|
state.check_viability(metrics)
|
|
return state
|