# SPDX-License-Identifier: AGPL-3.0-or-later """Realism — synthetic-file state across orchestrator ticks. The orchestrator's pre-realism file generator forgot every file the moment it was planted: each tick wrote a brand-new ``notes-{ts}.txt`` with a literal unix-epoch suffix. No edits, no rotation, no diurnal shape — three of the realism failures the migration is fixing. :class:`SyntheticFile` is the per-(decky, path) memory that lets the realism engine read back yesterday's ``TODO.md``, mutate it, write back the new body, and let the dashboard inspect the lineage. Pre-v1: schema lives directly in the SQLModel; no ``_migrate_*`` helper (per the project's "no new migrations pre-v1" rule — ``feedback_no_new_migrations_prev1.md``). Alembic lands at v1. """ from datetime import datetime, timezone from typing import Any, List from uuid import uuid4 from pydantic import BaseModel from sqlalchemy import Column, Index, Text, UniqueConstraint from sqlmodel import Field, SQLModel SYNTHETIC_FILE_BODY_LIMIT = 65536 """Cap on persisted ``synthetic_files.last_body`` bytes. Enforced by the repo on both insert and update — callers may pass the full body; the repo clips. Large blobs (DOCX/PDF, canary artifacts) are wasted disk on the master side; the decky filesystem holds the canonical bytes.""" class SyntheticFile(SQLModel, table=True): """One realism-planted file on one decky. The unique key is ``(decky_uuid, path)`` — there's at most one realism record per location, even if the planter has rotated the file (rotation updates ``edit_count`` and ``last_modified``, not a new row). ``last_body`` is capped — large blobs (DOCX/PDF, future canary artifacts) are truncated at write time. The edit-in-place flow (stage 3b) only needs the body when the content class supports body-level mutation (``note``, ``todo``, ``draft``, ``script``), so storing the canonical bytes for binary blobs would be wasted. ``content_hash`` is sha256 of the *body bytes only* — never of metadata or wrapper headers — so a hash compare is a cheap "did the body change?" check across edits. """ __tablename__ = "synthetic_files" __table_args__ = ( UniqueConstraint( "decky_uuid", "path", name="uq_synthetic_files_decky_path", ), Index("ix_synthetic_files_decky_modified", "decky_uuid", "last_modified"), ) uuid: str = Field(default_factory=lambda: str(uuid4()), primary_key=True) decky_uuid: str = Field(index=True, max_length=64) # Capped at 512 so the (decky_uuid, path) unique index fits MySQL's # 3072-byte utf8mb4 limit: (64+512)*4 = 2304 bytes. Real realism + # canary paths are well under (longest is # ``/home//Documents/Q3-Operations-Review.docx``, ~70 chars). path: str = Field(max_length=512) persona: str = Field(max_length=128) # EmailPersona.name content_class: str = Field(max_length=32, index=True) # ContentClass enum value created_at: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), index=True, ) last_modified: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), ) edit_count: int = Field(default=0) content_hash: str = Field(max_length=64) # sha256 hex last_body: str = Field( sa_column=Column("last_body", Text, nullable=False, default="") ) class SyntheticFilesResponse(BaseModel): total: int limit: int offset: int data: List[dict[str, Any]] class RealismConfig(SQLModel, table=True): """Operator-tunable realism knobs. Single-row-per-key schema: each row carries one piece of operator config (today: ``key="weights"`` → JSON encoding the planner's user/system/canary weights and canary probability). The planner reads in-memory module globals; the orchestrator worker refreshes those globals from this table on a periodic tick. UUID PK + unique key per ``feedback_uuid_over_natural_keys.md``. """ __tablename__ = "realism_config" uuid: str = Field(default_factory=lambda: str(uuid4()), primary_key=True) key: str = Field(max_length=64, unique=True, index=True) value: str = Field( sa_column=Column("value", Text, nullable=False, default="{}"), ) updated_at: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), )