Files
ThothII/harness/tht/memory/models.py
T
Codex 82e2c91f42
Publish documentation / publish (push) Successful in 1m27s
feat: implement memory and evidence administration with guided repairs
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation.

Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
2026-09-10 10:31:34 +02:00

113 lines
4.0 KiB
Python

"""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