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>
146 lines
8.3 KiB
Markdown
146 lines
8.3 KiB
Markdown
# 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_rule` into 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.
|