"""ChangeProposal — a structured diff the AI assistant (or an untrusted contributor) proposes, which a human approves/edits/rejects. Applying it routes each operation through the normal editing services, so the change passes the privacy engine and is audited as the approving human's action. See docs/design/change-proposal.md and CLAUDE.md non-negotiable #1. """ import uuid from datetime import datetime from sqlalchemy import DateTime, ForeignKey, String, Text from sqlalchemy import Enum as SAEnum from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from app.models.base import Base from app.models.enums import ChangeProposalOrigin, ChangeProposalStatus from app.models.mixins import SoftDelete, TenantScoped, Timestamps, UUIDPrimaryKey class ChangeProposal(Base, UUIDPrimaryKey, TenantScoped, Timestamps, SoftDelete): __tablename__ = "change_proposals" status: Mapped[ChangeProposalStatus] = mapped_column( SAEnum(ChangeProposalStatus, name="change_proposal_status"), default=ChangeProposalStatus.pending, server_default=ChangeProposalStatus.pending.value, index=True, ) origin: Mapped[ChangeProposalOrigin] = mapped_column( SAEnum(ChangeProposalOrigin, name="change_proposal_origin"), default=ChangeProposalOrigin.assistant, server_default=ChangeProposalOrigin.assistant.value, ) created_by_user_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL") ) summary: Mapped[str] = mapped_column(String(512)) rationale: Mapped[str | None] = mapped_column(Text) # The structured diff: a list of {op, entity_type, entity_id?, payload} dicts. operations: Mapped[list] = mapped_column(JSONB, nullable=False) reviewed_by_user_id: Mapped[uuid.UUID | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL") ) reviewed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) review_note: Mapped[str | None] = mapped_column(String(512)) apply_error: Mapped[str | None] = mapped_column(Text)