"""One review queue for every corpus the platform keeps knowledge in. Lives in ``core`` rather than in the support domain for the reason domain modules do not import each other: the security answer library will use the same queue, and a second copy of this table would mean two review pages, two sets of words for the same decision, and two places to fix a bug in. The table is deliberately narrow on identity: ``target_ref`` is a **string**, not a polymorphic foreign key. A real FK would have to point at one table, and this row's whole purpose is to be corpus-agnostic. The cost of that choice is that a target can vanish under a proposal — which is why :data:`~app.core.knowledge_loop.constants.STATUS_SUPERSEDED` exists and why the adapter re-reads its target before applying anything. Naive-UTC datetime discipline (codebase-wide MySQL/TiDB convention). """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import ( JSON, DateTime, ForeignKey, Index, String, Text, UniqueConstraint, func, ) from sqlalchemy.orm import Mapped, mapped_column from app.core.database import Base from app.core.knowledge_loop.constants import STATUS_OPEN from app.core.sql_types import GUID as Uuid class KnowledgeProposal(Base): """One machine-suggested change to a body of knowledge, awaiting a person.""" __tablename__ = "knowledge_proposals" __table_args__ = ( # At most ONE open proposal per (corpus, fingerprint). # # ``open_fingerprint`` carries the fingerprint while the row is open and # NULL once it is decided; MySQL lets a unique index hold any number of # NULLs, so history accumulates freely while the queue cannot grow two # rows saying the same thing. The alternative — checking for a duplicate # in Python before inserting — is a race the ingest paths would actually # lose: the weekly lint and a ticket close can propose the same alias # within the same second. UniqueConstraint( "corpus", "open_fingerprint", name="uq_knowledge_proposal_open", ), Index("idx_knowledge_proposal_queue", "corpus", "status", "created_at"), Index("idx_knowledge_proposal_target", "corpus", "target_ref"), Index("idx_knowledge_proposal_source", "source", "status"), ) id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4) corpus: Mapped[str] = mapped_column(String(32), nullable=False) kind: Mapped[str] = mapped_column(String(24), nullable=False) #: The page this proposal changes. NULL for a brand-new page. target_ref: Mapped[str | None] = mapped_column(String(64)) #: The other page, for merge / contradiction / link. related_ref: Mapped[str | None] = mapped_column(String(64)) #: One line a reviewer can scan without opening anything. title: Mapped[str] = mapped_column(String(255), nullable=False) #: Kind-specific. ``add_aliases`` carries ``{"aliases": [...]}``; #: ``append_section`` carries ``{"heading": ..., "body": ...}``; #: ``new_article`` carries the whole draft. The adapter owns the shape. payload: Mapped[dict | None] = mapped_column(JSON) #: Why the machine thinks so, in words. A queue of unexplained diffs is a #: queue that gets rubber-stamped. rationale: Mapped[str | None] = mapped_column(Text) #: Where it came from, in enough detail to go and look: a ticket id, a #: document token, an evaluation result id, the employee's own query. evidence: Mapped[dict | None] = mapped_column(JSON) source: Mapped[str] = mapped_column(String(24), nullable=False) source_ref: Mapped[str | None] = mapped_column(String(128)) #: Stable hash of "what this proposal asks for", used for deduplication. #: Always populated, including after the proposal is decided, so a later #: run can see that this was already asked and answered. fingerprint: Mapped[str] = mapped_column(String(64), nullable=False) #: The fingerprint while open, NULL once decided. See the unique constraint. open_fingerprint: Mapped[str | None] = mapped_column(String(64)) status: Mapped[str] = mapped_column(String(16), nullable=False, default=STATUS_OPEN) decided_by_person_id: Mapped[uuid.UUID | None] = mapped_column( Uuid, ForeignKey("identity_persons.id", ondelete="SET NULL"), nullable=True, ) decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) decision_note: Mapped[str | None] = mapped_column(Text) #: What accepting it produced or changed — the new article's id, the merged #: survivor, the link that was written. applied_ref: Mapped[str | None] = mapped_column(String(64)) #: Why applying failed, when it did. error: Mapped[str | None] = mapped_column(Text) 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(), ) __all__ = ["KnowledgeProposal"]