"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>
8.3 KiB
Writing a rule, a lesson, or a note that asserts a fact
Part of the using-scribe skill. Read it before create_rule,
create_project_rule, create_preference or create_lesson; when a lesson
arrives that names the situation you are actually in; and before filling
verify_with or expires_when on a note.
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
A rule has one of two homes, and the home IS its reach:
- Global — in a rulebook (
create_ruleinto a topic). It applies in every project, and reaches a session wherever the work matches it. A rulebook is a themed grouping of general rules (e.g. a review checklist), not a list of projects it binds — there is no subscribing a project to one. - Project (
create_project_rule) — anything specific to one project (its files, paths, quirks). It reaches only that project's sessions.
Names one project's specifics → project rule; a standard that holds wherever
the kind of work it describes happens → global. Never put project-specific
detail in a rulebook — it would reach every other project. A project that
departs from a global rule writes its own and links it with
relate_rules(kind="overrides"), which says why. A rule that turns out to be
in the wrong home — a project rule that holds everywhere, a global one only a
single project needs — moves with move_rule, which keeps its id, history,
areas and edges. Propose the move and make it on a yes.
Whichever home it gets, a rule needs when_to_apply. It is the only thing
that decides whether the rule is ever seen: nothing is preloaded, so a rule
with no trigger is not a quiet rule, it is an unreachable one. Write the moment
in the words a session actually produces — the command, the error, the
half-formed ask — not the category it belongs to.
Before writing one, ask what already covers that moment.
what_might_apply("the moment you are about to write a record for") — fifty
candidates and no bar, so an existing record cannot hide under a threshold the
way it can from search. When something already covers the moment, the reach
is that record: improve its trigger or its statement rather than standing a
second one beside it. Two records describing the same moment compete in one
ranked list against one budget, and the slot they take from each other is the
third candidate that would have said something different. Two records may
legitimately share a moment and say different things — a rule for what must
happen, a preference for how to report it. What this catches is the same thing
said twice at two strengths, which is worse than either alone: a session that
retrieves the softer copy has been told that binding guidance is optional.
First ask whether it's a rule at all. A rule is prose you have to remember and apply; Scribe's other entities are structure a tool can resolve and check. Visual standards belong in a design system, not a rulebook — a token can be inherited, resolved per mode, rendered to a stylesheet and diffed against code, and none of that survives being written as a rule. A repeatable procedure is a process; reusable code is a snippet. Reach for a rule when the thing really is a standing instruction about how to work.
Then ask what force it carries, by the question SKILL.md's reflex 2 asks: what
happens if someone doesn't do this? A standing instruction that merely costs
consistency is a preference (create_preference), and a transferable
insight that costs time is a lesson (create_lesson), keyed to the
situation it applies to so a later session meets it there. Both are first-class
outcomes of noticing something, not what's left when a rule proposal fails.
A lesson grows each time it proves itself
When one arrives and the situation it names is the one you are actually in, you
are the single reader placed to tell whether its trigger is keyed right and
whether its claim covers what you are seeing. update_lesson takes what you
now know: another incident added to what taught it, the claim stated more
exactly, or — the edit worth most — a trigger re-keyed to the situation that
really fired. A lesson nobody reaches is seldom wrong; far more often it is
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.
Supersession only fires once somebody has read a note and disagreed — which
is the case where it was already believed. A note asserting something about
someone else's software — what a service does on a duplicate upload, how a
forge numbers its CI runs, what an updater compares — can instead carry
verify_with (how to check it) and expires_when (the STATE that ends it:
"when the forge numbers runs per workflow", never "in six months").
notes_due_for_verification lists them least-recently-confirmed first, with
never-checked at the top; mark_note_verified records what you found, and
still_true=False deliberately writes nothing — a note whose check failed
is wrong rather than in a state worth recording, so it keeps its place.
The test is one question: could this note become false without anyone
editing it? If no, leave both fields empty. That is the normal case, and
an empty verify_with is the positive marker for "this is a decision, there
is nothing to go and check" — not an unfinished record. The sweep is only
worth reading while almost nothing is on it, so a check added out of
tidiness costs the whole surface, not just that note.
The sharper form of the same test: is the thing this note describes yours to change? If yes it is a decision — editing your own software is how it changes, and you will know you did it. Measured against a real corpus, every note that earned a check was about somebody ELSE's software: a signing service, a forge, a hub, an SDK, a model, a dependency set.
Three that look like candidates and are not:
- Resume pointers and "current state" notes. They go stale fastest of anything, which is exactly why they tempt — but the cure is to update or delete them, not to schedule a check. A sweep full of pointers is a sweep nobody reads.
- Measurements of your own system. They go false because you changed something, and you knew. A measurement earns a check only when what it measures is outside your control.
- A decision that RESTS on somebody else's behaviour. The decision is still a decision. Put the check on the note asserting the fact, and link the decision to it.
Not for tasks — a task's decay is its status, and a done issue records what
happened rather than asserting something that can go false. Not for snippets
either: verify_snippet compares the recorded location and code against the
repo, which is richer and already wired to drift detection.