CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 19s
CI & Build / integration (push) Failing after 43s
CI & Build / Python tests (push) Failing after 46s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / Build & push image (push) Skipped
A rule written before moments existed is mounted on nothing. Step 7 records, per (rule, moment), whether it belongs there and who said so: - rule_moment_judgments (migration 0118, backup v23): suggested / confirmed / rejected, from a pass, the signal, or an edit. Moment "" is "no moment fits". - The pass: rules_to_mount lists unjudged rules; propose_rule_moments records suggestions that mount nothing; rule_moment_proposals and judge_rule_moments put them to the operator. A confirm mounts, a reject is kept so the pair is never proposed again. Same service behind REST and a "Waiting on you" panel in Settings > Moments. - Edits are judgments: set_rule_moments, the one mount write path, confirms what was added and rejects what was removed in the same transaction. - The signal: scribe_moment.sh keeps a per-session acts ledger; when a rule is opened, scribe_record_opened.sh sends the last three minutes of it to /api/plugin/rule-opened. The acts resolve through the install's mappings; work.run and work.change are not evidence. Counted per distinct session with lesson_rules' evidence model, and once due the open returns one line asking the reader to offer the mount. - scribe_session_end.sh removes the session's scribe-moment files. Plugin 2026.10.05.2003. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
82 lines
3.5 KiB
Python
82 lines
3.5 KiB
Python
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),
|
|
}
|