feat(lessons): judged when written — a new lesson is offered its rules, and "no rule fits" is an answer (milestone 440 step 2, #4631)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m15s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m15s
CI & Build / Build & push image (push) Skipped
"Which rule is this lesson an instance of?" now has three recorded answers: a rule named (a confirmed link, #4630), no rule fits (new), or unjudged. - Model + migration 0112: lesson_no_rule (lesson_id PK, CASCADE from the note; why; judged_at). A table rather than a key in notes.data, because that mirror is re-composed from the body on every edit and would erase it. - Service (lesson_rules): set_no_rule rejects any confirmed link with the reason; a confirmation (set_lesson_rules or judge_link) deletes the answer; require_one_answer refuses both answers in one call before any write; judgments_for_lessons + attach_lesson_rules add rule_judgment (and no_rule) to every lesson payload; list_unjudged lists the open ones; rule_candidates searches rules with the lesson's claim + trigger at the explicit-search bar, None when the search could not run. - MCP: create_lesson/update_lesson take no_rule; an unanswered create returns rule_candidates, rule_judgment and a rule_hint; list_lessons(unjudged=true). - REST: the same on POST/PATCH /api/lessons and GET ?unjudged=1; create returns rule_candidates. - Backup v19: a lesson_no_rule section, export (full and per-user) and import. - Guidance: create_lesson docstring, writing-records.md in using-scribe (owner, pinned in test_guidance_ownership), create_rule docstring on linking the lessons a new rule governs. Plugin version minted. - Tests: door units, integration for the three states, the rejection reason, scoping, cascade; backup registries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,7 +78,7 @@ from scribe.models.rulebook import ( # noqa: E402, F401
|
||||
Rulebook, RulebookTopic, Rule, RuleRelation, rule_systems,
|
||||
)
|
||||
# After notes and rules: it foreign-keys both (milestone 440).
|
||||
from scribe.models.lesson_rule_link import LessonRuleLink # noqa: E402, F401
|
||||
from scribe.models.lesson_rule_link import LessonNoRule, LessonRuleLink # noqa: E402, F401
|
||||
from scribe.models.repo_binding import RepoBinding # noqa: E402, F401
|
||||
from scribe.models.forge_connection import ForgeConnection # noqa: E402, F401
|
||||
from scribe.models.code_shape import CodeShape, CodeShapeConsumer, CodeShapeEvent, CodeShapeUse # noqa: E402, F401
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text, UniqueConstraint
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, Text, UniqueConstraint, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
@@ -75,3 +75,38 @@ class LessonRuleLink(Base, CreatedAtMixin):
|
||||
"judged_at": iso(self.judged_at),
|
||||
"created_at": iso(self.created_at),
|
||||
}
|
||||
|
||||
|
||||
class LessonNoRule(Base):
|
||||
"""A judgment that no rule governs a lesson's situation (#4631).
|
||||
|
||||
The third answer to "which rule is this an instance of?", beside naming
|
||||
one and leaving it open. It is what lets an UNJUDGED lesson be told apart
|
||||
from one that was looked at and found to stand alone — and a lesson with
|
||||
no rule is the raw material #4634 reads for convergence, so the
|
||||
difference is the whole signal.
|
||||
|
||||
A row of its own rather than a key in the lesson's `data`: that column is
|
||||
a mirror re-composed from the body on every edit (services/lessons.py),
|
||||
so anything kept there that the body does not render is erased by the
|
||||
next rewording — and the lesson would silently read as unjudged again.
|
||||
|
||||
One row per lesson. Naming a rule later deletes it: "no rule fits" and
|
||||
"this rule fits" cannot both be the current answer.
|
||||
"""
|
||||
|
||||
__tablename__ = "lesson_no_rule"
|
||||
|
||||
lesson_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("notes.id", ondelete="CASCADE"), primary_key=True,
|
||||
)
|
||||
# Why none fits — required, because "no rule" with no reason cannot be
|
||||
# re-judged when a rule is later written for the situation.
|
||||
why: Mapped[str] = mapped_column(Text)
|
||||
judged_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {"why": self.why, "judged_at": iso(self.judged_at)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user