"""Authoritative Memory contracts, independent of workflow decision kinds.""" from datetime import datetime from typing import Literal, Self from pydantic import BaseModel, ConfigDict, Field, model_validator Family = Literal["domain_clarification", "sql_rule", "solved_question", "explained_error"] class Dependency(BaseModel): model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) database: str = Field(min_length=1, max_length=200) schema_name: str = Field(default="", max_length=200) table: str = Field(default="", max_length=200) column: str = Field(default="", max_length=200) @model_validator(mode="after") def structured(self) -> Self: if self.column and not self.table: raise ValueError("A column dependency requires a table") if self.table and not self.schema_name: raise ValueError("A table dependency requires a schema") return self class LinkInput(BaseModel): model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) target_id: str = Field(min_length=1, max_length=100) meaning: str = Field(min_length=1, max_length=1000) class CardInput(BaseModel): model_config = ConfigDict(extra="forbid", str_strip_whitespace=True) family: Family subject: str = Field(min_length=1, max_length=1000) detail: str = Field(default="", max_length=50000) scope: str = Field(min_length=1, max_length=10000) rationale: str = Field(default="", max_length=10000) question: str = Field(default="", max_length=10000) sql: str = Field(default="", max_length=100000) concepts: list[str] = Field(default_factory=list, max_length=100) dependencies: list[Dependency] = Field(default_factory=list, max_length=200) links: list[LinkInput] = Field(default_factory=list, max_length=200) @model_validator(mode="after") def valid_family(self) -> Self: if self.family == "solved_question" and (not self.question or not self.sql): raise ValueError("A solved question requires its question and approved SQL") if self.family == "explained_error" and (not self.detail or not self.rationale): raise ValueError("An explained error requires a correction and rationale") if self.family != "solved_question" and self.sql: raise ValueError("Only solved questions carry exemplar SQL") if any(not c.strip() or len(c) > 200 for c in self.concepts): raise ValueError("Concepts must contain between 1 and 200 characters") if len({link.target_id for link in self.links}) != len(self.links): raise ValueError("Each linked destination must be unique") return self class Card(CardInput): id: str workspace_id: str origin: Literal["manual", "workflow"] session_id: str | None = None decision_seq: int | None = None created_at: datetime updated_at: datetime revision: str indexed: bool = False class CardQuery(BaseModel): model_config = ConfigDict(extra="forbid") q: str = Field(default="", max_length=1000) family: Family | None = None concept: str = Field(default="", max_length=200) database: str = Field(default="", max_length=200) table: str = Field(default="", max_length=200) column: str = Field(default="", max_length=200) origin: Literal["manual", "workflow"] | None = None updated_after: datetime | None = None updated_before: datetime | None = None page: int = Field(default=1, ge=1) page_size: int = Field(default=25, ge=1, le=100) sort: Literal["updated_at", "created_at", "subject", "family"] = "updated_at" direction: Literal["asc", "desc"] = "desc" class MemoryError(Exception): code = "memory_operation_failed" status = 500 class MemoryUnavailable(MemoryError): code = "memory_unavailable" status = 503 class MemoryNotFound(MemoryError): code = "memory_not_found" status = 404 class MemoryForbidden(MemoryError): code = "memory_forbidden" status = 403 class MemoryConflict(MemoryError): code = "memory_conflict" status = 409