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

"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:
2026-10-01 13:25:27 -04:00
co-authored by Claude Opus 5.5
parent c8393975c3
commit f7d8dc2e55
15 changed files with 696 additions and 37 deletions
@@ -8,6 +8,7 @@ arrives that names the situation you are actually in; and before filling
## Contents
- Where a new rule goes — its home, its trigger, what already covers the moment
- A lesson grows each time it proves itself
- A lesson names the rule it is an instance of
- A note that asserts a fact can carry its own check
## Where a new rule goes
@@ -76,6 +77,29 @@ waiting in a situation nobody is in. One claim that has met the same failure
four times is worth more than four claims that each met it once, so when a
near-duplicate create hands back an existing id, that is the record to grow.
## A lesson names the rule it is an instance of
A lesson records one situation; a rule records the binding choice for a class
of them. Linking the two is what lets a rule be reached through the situations
that keep proving it — moments that resemble why it was written more closely
than its own wording does. A lesson never becomes a rule. It points at one.
The writer is best placed to say which rule, so answer while writing, in
`create_lesson` itself or in the `update_lesson` right after it:
- **`rule_ids=[…]`** — the rule or preference whose situation this is an
instance of.
- **`no_rule="why"`** — no rule governs it, in a line. That is a full answer,
not a gap: lessons that stand alone and keep landing in one situation are
what a missing rule looks like, and the reason is what lets the answer be
re-judged once a rule exists.
A lesson created with neither comes back with `rule_candidates`, the rules it
most resembles, each with its trigger. Read them against the situation the
lesson describes, not its topic — a rule about the same subsystem that governs
a different moment is not its rule — and give one of the two answers.
`list_lessons(unjudged=true)` gathers the ones still open.
## A note that asserts a fact can carry its own check
**A few notes assert a FACT, and those can carry their own check.**