docs(6205): lesson_rules comments state the reason, not the instance that taught it
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m10s
CI & Build / Build & push image (push) Canceled after 0s
CI & Build / Python tests (push) Canceled after 1m48s

Issue, milestone, snippet and rule-number citations are removed; the scope
docstring states the leak it prevents without naming the milestone that
closed it for the direct arms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:23:11 -04:00
co-authored by Claude Opus 5.5
parent 21a409eac1
commit 5605b863b2
+29 -28
View File
@@ -1,4 +1,4 @@
"""Lessons point at rules (milestone 440, #4196).
"""Lessons point at rules.
A lesson is a non-binding record of one situation; a rule is the binding
choice for a class of them. This service owns the link between the two — which
@@ -7,17 +7,18 @@ rule a lesson is an instance of — and both directions of reading it.
THE STATES (models/lesson_rule_link.py says why each exists):
`suggested` while evidence accumulates, `confirmed` or `rejected` once a
judgment is made. Every write here is a JUDGMENT, so every write lands as
confirmed or rejected; `suggested` rows come from the co-surfacing recorder
(#4637), never from a caller naming a rule.
confirmed or rejected; `suggested` rows come from the co-surfacing recorder,
never from a caller naming a rule.
THE THIRD ANSWER (#4631). "Which rule is this an instance of?" has three
THE THIRD ANSWER. "Which rule is this an instance of?" has three
answers, and only two of them are links: a rule named (confirmed), or NO RULE
FITS — a `lesson_no_rule` row carrying the reason. A lesson with neither is
UNJUDGED, and `list_unjudged` lists those. The three are kept apart because a
lesson that stands alone is the raw material for a rule nobody has written yet
(#4634), and that is unreadable if "nobody looked" means the same thing.
(see Convergence below), and that is unreadable if "nobody looked" means the
same thing.
THE SOFT LINK (#4637). When one hook request puts a lesson and a rule in
THE SOFT LINK. When one hook request puts a lesson and a rule in
front of the reader together, `co_surfaced` records the pair as evidence on a
SUGGESTED link, and once that evidence crosses a bar the next co-arrival asks
the reader to judge it. The evidence counts distinct SITUATIONS, not
@@ -25,7 +26,7 @@ arrivals: two records that resemble each other arrive together on every
similar prompt, and counting each time would measure their similarity over
and over rather than any relation between them.
ACL (rule 78). Linking changes what a lesson says about itself, so it needs
ACL (services/access.py). Linking changes what a lesson says about itself, so it needs
WRITE on the lesson (`access.can_write_note`, share-aware). It names a rule, so
it needs the rule to be one the caller may read — rules are owner-scoped, and
`rulebooks._fetch_owned_rule` / `_owned_rules_clause` are that check's two
@@ -67,8 +68,8 @@ _NO_RULE_NOTE = "no rule fits: "
# What `rule_judgment` reads on a lesson payload — the three answers.
LINKED, NO_RULE, UNJUDGED = "linked", "no_rule", "unjudged"
# THE SOFT-LINK EVIDENCE BAR (#4637) — defaults, stated as defaults (rules 32,
# 115): nothing about this install chose them.
# THE SOFT-LINK EVIDENCE BAR — defaults, stated as defaults: nothing about an
# install chose them.
#
# Three distinct situations before a pair is put to the reader. One is
# coincidence and two is a pattern only by courtesy; three different prompts
@@ -361,7 +362,7 @@ async def attach_lesson_rules(user_id: int, rows: list[dict], *, key: str = "id"
"""Add `rules` and `rule_judgment` (plus `no_rule` when that is the answer)
to each lesson payload row, in place — a fixed number of queries per page.
Fail-open (snippet #4286): this decorates a lesson the caller already has,
Fail-open: this decorates a lesson the caller already has,
so a failed lookup leaves the keys off and logs, rather than refusing the
lesson. An absent key reads as "not attached", never as "no rule".
"""
@@ -470,7 +471,7 @@ async def rule_candidates(
async def attach_rule_lessons(user_id: int, data: dict, rule_id: int) -> None:
"""Add `lessons` to a rule payload, in place, when there are any.
Present-only, as a rule's `systems` and `relations` are (#2483), and
Present-only, as a rule's `systems` and `relations` are, and
fail-open for the reason `attach_lesson_rules` gives.
"""
try:
@@ -487,8 +488,8 @@ def situation_key(arm: str, text: str) -> str:
"""A fingerprint of one situation, so a repeat counts once.
`arm` is part of the key — "p" for a prompt, "w" for a file being written,
"s" for a session (the rule↔moment signal, milestone 458 step 7), "d"
for a day (the misfire signal, step 7b, whose door carries no session) —
"s" for a session (the rule↔moment signal), "d" for a day (the misfire
signal, whose door carries no session) —
because each names a different kind of situation and must not collide.
On the prompt arm the text is the prompt: lowercased, split into word
tokens of _MIN_TOKEN or more, de-duplicated and sorted, so the same ask
@@ -657,7 +658,7 @@ async def _co_surfaced(user_id, lesson_ids, rule_ids, arm, situation, project_id
return _proposal_line(lid, lesson_title, rid, rule.title, rule.kind or "rule", ev)
# ── Reaching a rule through its lessons (#4633) ──────────────────────────────
# ── Reaching a rule through its lessons ──────────────────────────────────────
async def confirmed_lessons(user_id: int) -> set[int]:
@@ -666,8 +667,8 @@ async def confirmed_lessons(user_id: int) -> set[int]:
CONFIRMED ONLY, and the reason is the soft link's: a suggested link that
carried its rule would put the two side by side on every prompt that
found the lesson, which is exactly the co-arrival #4637 counts as
evidence — the suggestion would manufacture its own proof.
found the lesson, which is exactly the co-arrival the soft link counts
as evidence — the suggestion would manufacture its own proof.
Asked first and cheaply, so an arm on an install with no confirmed links
— every install, until someone judges one — never pays for a search.
@@ -695,11 +696,11 @@ async def confirmed_rules_in_scope(
) -> dict[int, list]:
"""{lesson_id: [Rule]} — each lesson's CONFIRMED rules that may surface here.
"May surface here" is the rule arms' own scope (milestone 414): a global
rule, or a rule of THIS project. A lesson is project-independent and
surfaces everywhere, so without this a lesson linked to project A's rule
would carry that rule into project B's sessions — the leak milestone 414
closed for the direct arms, reopened through a side door.
"May surface here" is the rule arms' own scope: a global rule, or a rule
of THIS project. A lesson is project-independent and surfaces everywhere,
so without this a lesson linked to project A's rule would carry that rule
into project B's sessions — a leak the direct arms' scope prevents,
reopened through a side door.
"""
from scribe.services.rulebooks import _owned_rules_clause
@@ -727,16 +728,16 @@ async def confirmed_rules_in_scope(
return out
# ── Convergence: lessons with no rule that keep landing in one place (#4634) ─
# ── Convergence: lessons with no rule that keep landing in one place ────────
#
# A lesson answered "no rule fits" is a situation nothing binds. One is a
# lesson. Several that resemble each other are what a missing rule looks like
# from the outside — the same moment met again and again, each time written
# down as advice. This is noticed at the WRITE, when the newest of them is
# answered, and never by a sweep or a timer (#4183): the reader is in the
# answered, and never by a sweep or a timer: the reader is in the
# situation then, and a nudge arriving anywhere else is one nobody acts on.
#
# WHAT "RESEMBLE" MEANS (#5193). Not a fixed similarity. Lessons are written
# WHAT "RESEMBLE" MEANS. Not a fixed similarity. Lessons are written
# in one register — a sentence of advice and the moment it applies — so an
# embedder scores any two of them well above two unrelated texts, and a
# constant bar sits inside that band: every no-rule lesson then "resembles"
@@ -750,10 +751,10 @@ async def confirmed_rules_in_scope(
#
# And resemblance must be MUTUAL, among every member, not just to the lesson
# being answered. A lesson broad enough to stand near many others is a hub,
# and hub-and-spoke is how three unrelated lessons used to make a "group"
# through one general one. A clique is the shape of one situation met again.
# and hub-and-spoke lets three unrelated lessons make a "group" through one
# general one. A clique is the shape of one situation met again.
#
# DEFAULTS, stated as defaults (rules 32, 115). Three lessons — the new one
# DEFAULTS, stated as defaults. Three lessons — the new one
# and two it resembles — is the smallest group that is a pattern rather than
# a pair. One spread above the background is a modest bar for one direction
# alone; the strictness comes from requiring it of every pair in both
@@ -834,7 +835,7 @@ async def convergence_for(user_id: int, lesson_id: int) -> dict | None:
"""The convergence a newly answered no-rule lesson completes, or None.
Fail-open: this decorates a write that already succeeded, so a failed
search leaves the response as it was (snippet #4286's reasoning).
search leaves the response as it was.
"""
try:
return await _convergence_for(user_id, lesson_id)