"""Spatial and temporal primitives: locations, time windows, and whereabouts. Times are integer minutes since midnight to keep alibi reasoning exact and cheap. """ from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field, model_validator from ..constants import DAY_MINUTES class Location(BaseModel): model_config = ConfigDict(frozen=True) loc_id: str name: str description: str = "" adjacent_to: tuple[str, ...] = () class TimeWindow(BaseModel): """A closed interval [start_min, end_min] within a single day.""" model_config = ConfigDict(frozen=True) start_min: int = Field(ge=0, le=DAY_MINUTES) end_min: int = Field(ge=0, le=DAY_MINUTES) @model_validator(mode="after") def _ordered(self) -> TimeWindow: if self.end_min < self.start_min: raise ValueError(f"end_min {self.end_min} precedes start_min {self.start_min}") return self def contains(self, minute: int) -> bool: return self.start_min <= minute <= self.end_min def covers(self, other: TimeWindow) -> bool: return self.start_min <= other.start_min and other.end_min <= self.end_min def overlaps(self, other: TimeWindow) -> bool: return self.start_min <= other.end_min and other.start_min <= self.end_min class WhereaboutsSegment(BaseModel): """Where a person actually was during one slice of the murder window.""" model_config = ConfigDict(frozen=True) window: TimeWindow loc_id: str activity: str = "" co_present_sus_ids: tuple[str, ...] = () class AlibiSegment(BaseModel): """Where a suspect *claims* to have been, with any claimed witnesses.""" model_config = ConfigDict(frozen=True) window: TimeWindow loc_id: str witness_sus_ids: tuple[str, ...] = () class StatedAlibi(BaseModel): model_config = ConfigDict(frozen=True) claim_text: str claimed_segments: tuple[AlibiSegment, ...] = ()