from datetime import datetime from sqlalchemy import BigInteger, DateTime, ForeignKey, Text, UniqueConstraint from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from scribe.models import Base from scribe.models.base import CreatedAtMixin, iso from scribe.models.lesson_rule_link import CONFIRMED, LINK_STATES, REJECTED, SUGGESTED # The same three states as a lesson's link to a rule, for the same reasons — # re-exported so a reader of this table need not know where they were first # named. CHECK ck_rule_moment_judgments_state (migration 0118, rule 36). JUDGMENT_STATES = LINK_STATES __all__ = [ "CONFIRMED", "JUDGMENT_STATES", "NO_MOMENT", "REJECTED", "SUGGESTED", "SOURCES", "RuleMomentJudgment", ] # The moment recorded for "this rule is about WHAT, not WHEN": no moment in # the catalog is when it applies, and it is right to reach it by meaning. A # row rather than an absence, for the reason LessonNoRule is one — a rule # looked at and found to have no moment must not read as one nobody looked at, # or every pass over the corpus would propose it again. NO_MOMENT = "" # Where a judgment came from. `pass` — an agent read the rule and proposed; # `signal` — the rule kept being opened just after a moment fired; `edit` — a # person changed the rule's moments directly, which is a judgment too. SOURCES = ("pass", "signal", "edit") class RuleMomentJudgment(Base, CreatedAtMixin): """Whether a rule belongs on a moment, and who said so (milestone 458 step 7). A mount (`rule_moments`) is the current answer; this is the reasoning behind it and the answers that are NOT mounts. One row per (rule, moment): - ``suggested`` — proposed by a pass, or by repeated opens after the moment fired. Delivers nothing: a suggestion that mounted itself would manufacture the opens it counts. - ``confirmed`` — a judgment put the rule on the moment. The mount exists. - ``rejected`` — a judgment said it does not belong there, kept with its reason so the pair is never proposed again, by either source. Moment ``""`` (NO_MOMENT), confirmed, is "no moment fits this rule". """ __tablename__ = "rule_moment_judgments" id: Mapped[int] = mapped_column(BigInteger, primary_key=True) rule_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("rules.id", ondelete="CASCADE"), index=True, ) moment: Mapped[str] = mapped_column(Text) state: Mapped[str] = mapped_column(Text, default=SUGGESTED, server_default=SUGGESTED) source: Mapped[str] = mapped_column(Text, default="pass", server_default="pass") # Why — for a suggestion, what the proposer read in the rule; for a # judgment, why it was confirmed or rejected. note: Mapped[str | None] = mapped_column(Text, nullable=True) # The co-occurrence evidence (signal source), in lesson_rules' shape. evidence: Mapped[dict | None] = mapped_column(JSONB, nullable=True) judged_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) __table_args__ = ( UniqueConstraint("rule_id", "moment", name="uq_rule_moment_judgments_pair"), ) def to_dict(self) -> dict: return { "rule_id": self.rule_id, "moment": self.moment, "state": self.state, "source": self.source, "note": self.note or "", "evidence": self.evidence or {}, "judged_at": iso(self.judged_at), "created_at": iso(self.created_at), }