feat(lessons): convergence is named at the write — no-rule lessons that keep landing in one situation suggest a rule (milestone 440 step 5, #4634)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 59s
CI & Build / Python tests (push) Successful in 1m45s
CI & Build / Build & push image (push) Successful in 27s

When a lesson is answered "no rule fits" (create_lesson / update_lesson on
both doors), the response looks for other no-rule lessons it resembles and,
once there are CONVERGENCE_LESSONS (3) of them, carries `convergence`: the
members, their incidents and projects, and a hint to draft the missing rule
with create_rule (operator approval as always) and point each lesson at it —
or to leave them as lessons when no single choice is right every time.

- convergence_group is the pure bar: distinct LESSONS count, incidents never
  stand in for them (one broad lesson cannot trigger it), and a group whose
  sources all point at one incident is one event written up several times.
- convergence_for searches lessons by the new one's claim + trigger
  (trigger_title) at CONVERGENCE_THRESHOLD 0.65 — above the menu's "worth
  showing", below the duplicate gate's "same record" — then keeps the ones
  with a lesson_no_rule answer. Fail-open. No sweep, no timer (#4183).
- Defaults stated as defaults (rules 32, 115).
- Tests: the bar (pure), the search with stubs, the door, and the no-rule
  filter against Postgres; conftest stubs convergence_for for unit tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:31:15 -04:00
co-authored by Claude Opus 5.5
parent f34249a2d8
commit 6d3dca0af5
6 changed files with 259 additions and 3 deletions
@@ -347,3 +347,15 @@ async def test_a_project_rule_stays_in_its_project_even_through_a_lesson(world):
async def test_a_stranger_reaches_no_rule_through_someone_elses_link(world):
await links_svc.set_lesson_rules(world["owner"], world["lesson"], [world["r1"]])
assert await links_svc.confirmed_lessons(world["stranger"]) == set()
# ── #4634: which lessons count toward convergence ───────────────────────────
async def test_only_answered_lessons_are_no_rule_lessons(world):
owner = world["owner"]
other = await lessons_svc.create_lesson(
owner, what="Another overrun", when_to_apply="a deploy job overran",
)
await links_svc.set_no_rule(owner, world["lesson"], "specific to this host")
assert await links_svc._no_rule_ids([world["lesson"], other.id]) == {world["lesson"]}