| """SQLAlchemy 2.0 ORM models for the engine's Postgres tables. |
| |
| Spec: docs/Specs.md §9.1, docs/07-DataLayer.md. |
| Invariant I-7: every persisted row carries `subreddit_id`. Queries that |
| forget to filter by `subreddit_id` violate the isolation guarantee from |
| [10-ReliabilityAndSafety.md] and ADR-0004. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import uuid |
| from datetime import datetime |
|
|
| from sqlalchemy import ( |
| BigInteger, |
| Boolean, |
| CheckConstraint, |
| DateTime, |
| Float, |
| ForeignKey, |
| Index, |
| Integer, |
| String, |
| Text, |
| UniqueConstraint, |
| func, |
| ) |
| from sqlalchemy.dialects.postgresql import JSONB, UUID |
| from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship |
|
|
|
|
| class Base(DeclarativeBase): |
| pass |
|
|
|
|
| |
|
|
| PERSONALITIES = ("strict", "balanced", "lenient") |
| REGIONS = ("US", "EU", "UK", "IN", "Global") |
| TIER_OVERRIDES = ("auto", "fast", "standard", "deep") |
|
|
| RISK_TIERS = ("HIGH", "MEDIUM", "LOW") |
| RECOMMENDATIONS = ("REMOVE", "APPROVE", "ESCALATE", "LOCK", "NO_RECOMMENDATION") |
| STRATEGY_TIERS = ("FAST", "STANDARD", "DEEP") |
| TARGET_KINDS = ("comment", "post") |
| TOOL_STATUSES = ("success", "failure", "skipped", "timeout") |
| INVESTIGATION_STATUSES = ("pending", "completed", "failed") |
| USER_RISK_TIERS = ("new", "trusted", "neutral", "watched") |
| FEEDBACK_ACTIONS = ("REMOVE", "APPROVE", "ESCALATE", "LOCK") |
| FEEDBACK_SOURCES = ("verdict_card", "reddit_native") |
|
|
|
|
| def _check(col: str, values: tuple[str, ...], name: str) -> CheckConstraint: |
| quoted = ",".join(f"'{v}'" for v in values) |
| return CheckConstraint(f"{col} IN ({quoted})", name=name) |
|
|
|
|
| |
|
|
|
|
| class SubredditProfile(Base): |
| """One row per subreddit install. Per docs/05-Memory.md §personality.""" |
|
|
| __tablename__ = "subreddit_profile" |
|
|
| subreddit_id: Mapped[str] = mapped_column(String(20), primary_key=True) |
| name: Mapped[str] = mapped_column(String(64), nullable=False) |
| personality: Mapped[str] = mapped_column(String(16), nullable=False, default="balanced") |
| rules: Mapped[str] = mapped_column(Text, default="", nullable=False) |
| region: Mapped[str] = mapped_column(String(8), nullable=False, default="Global") |
| cold_start_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
| calibration_weights: Mapped[dict[str, object]] = mapped_column( |
| JSONB, nullable=False, default=dict |
| ) |
| show_cost_in_dashboard: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) |
| kill_switch: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) |
| tier_override: Mapped[str] = mapped_column(String(16), nullable=False, default="auto") |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), nullable=False, server_default=func.now() |
| ) |
| updated_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), |
| nullable=False, |
| server_default=func.now(), |
| onupdate=func.now(), |
| ) |
|
|
| __table_args__ = ( |
| _check("personality", PERSONALITIES, "ck_subreddit_profile_personality"), |
| _check("region", REGIONS, "ck_subreddit_profile_region"), |
| _check("tier_override", TIER_OVERRIDES, "ck_subreddit_profile_tier_override"), |
| ) |
|
|
|
|
| class UserMemory(Base): |
| """Per-(subreddit, user) moderation memory. Exposed in UI as a tier label only.""" |
|
|
| __tablename__ = "user_memory" |
|
|
| id: Mapped[uuid.UUID] = mapped_column( |
| UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 |
| ) |
| subreddit_id: Mapped[str] = mapped_column( |
| String(20), |
| ForeignKey("subreddit_profile.subreddit_id", ondelete="CASCADE"), |
| nullable=False, |
| ) |
| user_id: Mapped[str] = mapped_column(String(20), nullable=False) |
| risk_tier: Mapped[str] = mapped_column(String(16), nullable=False, default="new") |
| prior_violations: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
| prior_approvals: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
| last_seen_at: Mapped[datetime | None] = mapped_column( |
| DateTime(timezone=True), nullable=True |
| ) |
| detail: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False, default=dict) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), nullable=False, server_default=func.now() |
| ) |
| updated_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), |
| nullable=False, |
| server_default=func.now(), |
| onupdate=func.now(), |
| ) |
|
|
| __table_args__ = ( |
| UniqueConstraint("subreddit_id", "user_id", name="uq_user_memory_sub_user"), |
| Index("ix_user_memory_subreddit", "subreddit_id"), |
| Index("ix_user_memory_user", "user_id"), |
| _check("risk_tier", USER_RISK_TIERS, "ck_user_memory_risk_tier"), |
| ) |
|
|
|
|
| class ThreadMemory(Base): |
| """Per-(subreddit, post) thread memory. Tracks mod actions and escalation. |
| |
| Spec: docs/05-Memory.md §4. |
| """ |
|
|
| __tablename__ = "thread_memory" |
|
|
| id: Mapped[uuid.UUID] = mapped_column( |
| UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 |
| ) |
| subreddit_id: Mapped[str] = mapped_column( |
| String(20), |
| ForeignKey("subreddit_profile.subreddit_id", ondelete="CASCADE"), |
| nullable=False, |
| ) |
| post_id: Mapped[str] = mapped_column(String(20), nullable=False) |
| mod_actions_taken: Mapped[list[object]] = mapped_column( |
| JSONB, nullable=False, default=list |
| ) |
| participants_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
| last_summary: Mapped[str] = mapped_column(Text, nullable=False, default="") |
| last_summary_at: Mapped[datetime | None] = mapped_column( |
| DateTime(timezone=True), nullable=True |
| ) |
| detail: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False, default=dict) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), nullable=False, server_default=func.now() |
| ) |
| updated_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), |
| nullable=False, |
| server_default=func.now(), |
| onupdate=func.now(), |
| ) |
|
|
| __table_args__ = ( |
| UniqueConstraint("subreddit_id", "post_id", name="uq_thread_memory_sub_post"), |
| Index("ix_thread_memory_subreddit", "subreddit_id"), |
| Index("ix_thread_memory_post", "post_id"), |
| ) |
|
|
|
|
| class Investigation(Base): |
| """One row per Engine /investigate call. The audit-trail backbone.""" |
|
|
| __tablename__ = "investigation" |
|
|
| id: Mapped[uuid.UUID] = mapped_column( |
| UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 |
| ) |
| correlation_id: Mapped[str] = mapped_column(String(80), nullable=False, unique=True) |
| subreddit_id: Mapped[str] = mapped_column( |
| String(20), |
| ForeignKey("subreddit_profile.subreddit_id", ondelete="CASCADE"), |
| nullable=False, |
| ) |
| target_kind: Mapped[str] = mapped_column(String(8), nullable=False) |
| target_id: Mapped[str] = mapped_column(String(20), nullable=False) |
| target_body: Mapped[str] = mapped_column(Text, nullable=False, default="") |
| target_author_id: Mapped[str] = mapped_column(String(20), nullable=False, default="") |
|
|
| tier: Mapped[str] = mapped_column(String(16), nullable=False) |
| status: Mapped[str] = mapped_column(String(16), nullable=False, default="pending") |
|
|
| |
| risk_tier: Mapped[str | None] = mapped_column(String(16), nullable=True) |
| recommendation: Mapped[str | None] = mapped_column(String(32), nullable=True) |
| calibrated_confidence: Mapped[float | None] = mapped_column(Float, nullable=True) |
| rationale: Mapped[str | None] = mapped_column(Text, nullable=True) |
| confidence_breakdown: Mapped[dict[str, object] | None] = mapped_column(JSONB, nullable=True) |
|
|
| model_reasoner: Mapped[str] = mapped_column(String(64), nullable=False, default="") |
| model_summarizer: Mapped[str] = mapped_column(String(64), nullable=False, default="") |
| cost_usd: Mapped[float] = mapped_column(Float, nullable=False, default=0.0) |
| latency_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
| input_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
| output_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
|
|
| validation_flag: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) |
| degraded: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) |
| cold_start: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) |
|
|
| started_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), nullable=False, server_default=func.now() |
| ) |
| completed_at: Mapped[datetime | None] = mapped_column( |
| DateTime(timezone=True), nullable=True |
| ) |
|
|
| evidence: Mapped[list[Evidence]] = relationship( |
| "Evidence", back_populates="investigation", cascade="all, delete-orphan" |
| ) |
|
|
| __table_args__ = ( |
| Index("ix_investigation_subreddit", "subreddit_id"), |
| Index("ix_investigation_target", "target_id"), |
| Index("ix_investigation_correlation", "correlation_id"), |
| _check("target_kind", TARGET_KINDS, "ck_investigation_target_kind"), |
| _check("tier", STRATEGY_TIERS, "ck_investigation_tier"), |
| _check("status", INVESTIGATION_STATUSES, "ck_investigation_status"), |
| CheckConstraint( |
| "risk_tier IS NULL OR risk_tier IN ('HIGH','MEDIUM','LOW')", |
| name="ck_investigation_risk_tier", |
| ), |
| CheckConstraint( |
| "recommendation IS NULL OR recommendation IN " |
| "('REMOVE','APPROVE','ESCALATE','LOCK','NO_RECOMMENDATION')", |
| name="ck_investigation_recommendation", |
| ), |
| CheckConstraint( |
| "calibrated_confidence IS NULL OR " |
| "(calibrated_confidence >= 0 AND calibrated_confidence <= 1)", |
| name="ck_investigation_confidence_range", |
| ), |
| ) |
|
|
|
|
| class Evidence(Base): |
| """Tool results that fed the verdict. Stable `ev-N` ids per investigation.""" |
|
|
| __tablename__ = "evidence" |
|
|
| id: Mapped[uuid.UUID] = mapped_column( |
| UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 |
| ) |
| investigation_id: Mapped[uuid.UUID] = mapped_column( |
| UUID(as_uuid=True), |
| ForeignKey("investigation.id", ondelete="CASCADE"), |
| nullable=False, |
| ) |
| subreddit_id: Mapped[str] = mapped_column(String(20), nullable=False) |
| evidence_id: Mapped[str] = mapped_column(String(16), nullable=False) |
| tool: Mapped[str] = mapped_column(String(32), nullable=False) |
| status: Mapped[str] = mapped_column(String(16), nullable=False, default="success") |
| summary: Mapped[str] = mapped_column(String(280), nullable=False, default="") |
| detail: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False, default=dict) |
| latency_ms: Mapped[int] = mapped_column(Integer, nullable=False, default=0) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), nullable=False, server_default=func.now() |
| ) |
|
|
| investigation: Mapped[Investigation] = relationship("Investigation", back_populates="evidence") |
|
|
| __table_args__ = ( |
| UniqueConstraint( |
| "investigation_id", "evidence_id", name="uq_evidence_investigation_evid" |
| ), |
| Index("ix_evidence_investigation", "investigation_id"), |
| Index("ix_evidence_subreddit", "subreddit_id"), |
| _check("status", TOOL_STATUSES, "ck_evidence_status"), |
| ) |
|
|
|
|
| class Feedback(Base): |
| """Moderator alignment with a verdict. Drives cold-start counter + calibration.""" |
|
|
| __tablename__ = "feedback" |
|
|
| id: Mapped[uuid.UUID] = mapped_column( |
| UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 |
| ) |
| correlation_id: Mapped[str] = mapped_column(String(80), nullable=False) |
| subreddit_id: Mapped[str] = mapped_column(String(20), nullable=False) |
| target_id: Mapped[str] = mapped_column(String(20), nullable=False) |
| mod_action: Mapped[str] = mapped_column(String(16), nullable=False) |
| raw_action: Mapped[str] = mapped_column(String(32), nullable=False, default="") |
| moderator_id: Mapped[str] = mapped_column(String(20), nullable=False, default="") |
| moderator_name: Mapped[str] = mapped_column(String(64), nullable=False, default="") |
| source: Mapped[str] = mapped_column(String(32), nullable=False) |
| aligned: Mapped[bool | None] = mapped_column(Boolean, nullable=True) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), nullable=False, server_default=func.now() |
| ) |
|
|
| __table_args__ = ( |
| Index("ix_feedback_correlation", "correlation_id"), |
| Index("ix_feedback_subreddit", "subreddit_id"), |
| Index("ix_feedback_target", "target_id"), |
| _check("mod_action", FEEDBACK_ACTIONS, "ck_feedback_mod_action"), |
| _check("source", FEEDBACK_SOURCES, "ck_feedback_source"), |
| ) |
|
|
|
|
| class AuditLog(Base): |
| """Immutable event log. Append-only; 90d retention per docs/Specs.md §13.3.""" |
|
|
| __tablename__ = "audit_log" |
|
|
| id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) |
| subreddit_id: Mapped[str] = mapped_column(String(20), nullable=False) |
| correlation_id: Mapped[str | None] = mapped_column(String(80), nullable=True) |
| event_type: Mapped[str] = mapped_column(String(64), nullable=False) |
| actor: Mapped[str] = mapped_column(String(64), nullable=False, default="system") |
| detail: Mapped[dict[str, object]] = mapped_column(JSONB, nullable=False, default=dict) |
| created_at: Mapped[datetime] = mapped_column( |
| DateTime(timezone=True), nullable=False, server_default=func.now() |
| ) |
|
|
| __table_args__ = ( |
| Index("ix_audit_log_subreddit_time", "subreddit_id", "created_at"), |
| Index("ix_audit_log_correlation", "correlation_id"), |
| Index("ix_audit_log_event_type", "event_type"), |
| ) |
|
|