"""Pydantic models for world artifacts, runtime state, claims, and events. These mirror the YAML frontmatter / JSON shapes described in PLAN.md sections 6, 7, and 8. Artifacts are authored as markdown-with-frontmatter; these models validate the machine-readable parts after parsing. """ from __future__ import annotations from datetime import UTC, datetime from enum import StrEnum from typing import Any, Literal from pydantic import BaseModel, Field def utcnow_iso() -> str: """ISO-8601 UTC timestamp used across ledgers and events.""" return datetime.now(UTC).isoformat() # -------------------------------------------------------------------------- # World artifacts (Section 6) # -------------------------------------------------------------------------- class TimelineSlice(BaseModel): """One row of the structured movement schedule — the spine of the case.""" time_slice: str character: str location: str action: str observed_by: list[str] = Field(default_factory=list) class Timeline(BaseModel): slices: list[TimelineSlice] = Field(default_factory=list) def for_character(self, name: str) -> list[TimelineSlice]: return [s for s in self.slices if s.character == name] def slice_by_id(self, time_slice: str) -> list[TimelineSlice]: return [s for s in self.slices if s.time_slice == time_slice] class EnvObject(BaseModel): """An authored object/detail in the environment with its true state.""" id: str location: str description_true: str evidential: bool = False clue: str | None = None visible_by_default: bool = True class ClueNode(BaseModel): """A discoverable clue node with unlock dependencies and exonerations.""" id: str reveals: str sources: list[str] = Field(default_factory=list) unlocks: list[str] = Field(default_factory=list) exonerates: list[str] = Field(default_factory=list) required_for_solution: bool = False # Optional prerequisites; if absent, derived from other nodes' `unlocks`. requires: list[str] = Field(default_factory=list) CrackBehavior = Literal[ "deflect", "silence", "anger", "lawyer_up", "leave", "partial_confess" ] SecretKind = Literal["guilty", "innocent"] Role = Literal["victim", "suspect", "witness", "accomplice"] class KnowledgeBoundary(BaseModel): witnessed: list[str] = Field(default_factory=list) topics_known: list[str] = Field(default_factory=list) topics_unknowable: list[str] = Field(default_factory=list) class CharacterCard(BaseModel): """Full character model. Frontmatter (machine) + prose (voice).""" name: str role: Role = "suspect" guilty: bool = False truth: str = "" knows: KnowledgeBoundary = Field(default_factory=KnowledgeBoundary) cover: str = "" never_admit: list[str] = Field(default_factory=list) cracks_when: str = "" crack_behavior: CrackBehavior = "deflect" tells: list[str] = Field(default_factory=list) secret_kind: SecretKind = "guilty" exoneration: str | None = None # Prose voice section (after frontmatter). voice: str = "" class Alibi(BaseModel): """A pre-authored shared lie rehearsed by accomplices.""" id: str characters: list[str] = Field(default_factory=list) agreed_facts: str = "" agreed_timeline: str = "" cover_per_character: dict[str, str] = Field(default_factory=dict) # The points where the alibi is actually false vs. the real timeline. seams: list[str] = Field(default_factory=list) body: str = "" class Solution(BaseModel): """GROUND TRUTH. Engine-only; never shown to the player pre-accusation.""" culprit: str means: str motive: str opportunity: str true_sequence: str = "" body: str = "" class WorldMeta(BaseModel): world_id: str created: str = Field(default_factory=utcnow_iso) seed: str = "" one_line: str = "" setting: str = "" twist_tag: str = "" play_count: int = 0 title: str = "" # -------------------------------------------------------------------------- # Runtime state (Section 7) # -------------------------------------------------------------------------- Polarity = Literal["affirm", "deny", "neutral"] TruthValue = Literal["true", "false", "unknown"] class Claim(BaseModel): """A structured proposition extracted from a character utterance.""" claim_id: str topic: str proposition: str turn: int polarity: Polarity = "neutral" engine_truth_value: TruthValue = "unknown" class LedgerEntry(BaseModel): """One committed utterance: verbatim + structured claims.""" turn: int raw: str claims: list[Claim] = Field(default_factory=list) ts: str = Field(default_factory=utcnow_iso) class CharacterLedger(BaseModel): character: str entries: list[LedgerEntry] = Field(default_factory=list) @property def claims(self) -> list[Claim]: out: list[Claim] = [] for e in self.entries: out.extend(e.claims) return out class TranscriptEvent(BaseModel): """One ordered line in the session transcript.""" turn: int kind: Literal[ "player", "character", "world", "crack", "confront", "accuse", "system", ] actor: str = "" # character/world name, or "player" text: str ts: str = Field(default_factory=utcnow_iso) meta: dict[str, Any] = Field(default_factory=dict) class SessionStatus(StrEnum): active = "active" solved = "solved" closed = "closed" class AccusationResult(BaseModel): culprit_named: str culprit_correct: bool means_supported: bool motive_supported: bool opportunity_supported: bool grade: Literal["solved", "right_culprit_unproven", "wrong"] debrief: str = "" missed_clues: list[str] = Field(default_factory=list) class SessionState(BaseModel): session_id: str world_id: str started_at: str = Field(default_factory=utcnow_iso) status: SessionStatus = SessionStatus.active turn: int = 0 discovered_clues: list[str] = Field(default_factory=list) cracked: list[str] = Field(default_factory=list) # characters in crack state accusation: AccusationResult | None = None # -------------------------------------------------------------------------- # Token ledger (Section 4) # -------------------------------------------------------------------------- class UsageRecord(BaseModel): ts: str = Field(default_factory=utcnow_iso) world_id: str = "" session_id: str = "" task: str = "" tier: str = "" provider: str = "" model: str = "" prompt_tokens: int = 0 completion_tokens: int = 0 total_tokens: int = 0 ok: bool = True retries: int = 0