feat(learning): lesson synthesis — merge overlapping lessons into richer super-lessons (#158 / INV-LRN8)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

הפתרון-האמיתי לחיתוך-השקט limit=15 (#157): במקום ערימת לקחים גולמיים חופפים, ממזגים
לקחי-סגנון דומים ל"לקח-על" אחד עשיר ומוכלל — הסט שזורם לכותב קטֵן ומשתבח (Authorial
Style Profiling). מחקה את סינתזת-הקנוני (V41/INV-LRN6) על אותה טבלה (G2, אין מאגר-מקביל).

מנגנון (services/lesson_synthesis.py, מסלול-יחיד):
- אשכול greedy לפי cosine (LESSON_SYNTH_CLUSTER_THRESHOLD) בתוך shard practice_area+category.
- מיזוג ע"י claude_session מעוגן-מקור (INV-AH, סגנון-בלבד INV-LRN5, abstain) + שער-drift
  (cosine מול centroid ≥ LESSON_SYNTH_DRIFT_FLOOR).
- לקח-על נכתב source='synthesis' + synthesized_from; המקורות→review_status='superseded'.
- שער מדורג-הפיך (הכרעת-יו"ר): מקורות approved → לקח-על approved (זורם), veto-יו"ר משחזר
  את המקורות (db.revert_lesson_synthesis, מחובר ל-PATCH lessons).
- idempotency: lookup-cosine מול synthesis קיים לפני INSERT.

נגזרות: SCHEMA_V46 (embedding vector(1024) + synthesized_from + ivfflat); כלי-MCP
lesson_synthesize_pending; scripts/backfill_lesson_synthesis.py (--dry-run/--apply, audit CSV);
config LESSON_SYNTH_*; spec INV-LRN8; SCRIPTS.md.

get_recent_decision_lessons ללא שינוי — superseded יוצא (מסנן approved), synthesis נכנס.
UI badges (synthesis/superseded) נדחים לשער-העיצוב (מוגנים ב-fallback, ללא קריסה).

בדיקות: py_compile ✓ · leak-guard G12 ✓ · smoke-test טהור לאשכול/cosine/centroid ✓.
אימות functional מלא (dry-run מול DB+voyage+claude CLI) — בהוסט אחרי-deploy, כמו V41.

Invariants: G2 (מסלול-יחיד, אותה טבלה), INV-AH (עיגון+drift), INV-LRN1/G10 (שער מדורג-הפיך),
INV-LRN5 (סגנון-בלבד), INV-LRN8 (חדש). depends-on #157/#159.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:24:42 +00:00
parent 9455da7567
commit 14ab7b0cae
9 changed files with 627 additions and 0 deletions

View File

@@ -275,6 +275,19 @@ HALACHA_CANONICAL_SYNTH_MODEL = os.environ.get("HALACHA_CANONICAL_SYNTH_MODEL",
HALACHA_CANONICAL_SYNTH_EFFORT = os.environ.get("HALACHA_CANONICAL_SYNTH_EFFORT", "high")
HALACHA_CANONICAL_SYNTH_DRIFT_FLOOR = float(os.environ.get("HALACHA_CANONICAL_SYNTH_DRIFT_FLOOR", "0.80"))
# Lesson synthesis (#158 / INV-LRN8) — mirrors the canonical-halacha synthesis above
# for decision_lessons: cluster overlapping style lessons (cosine ≥ CLUSTER_THRESHOLD,
# within a practice_area+category shard) and merge each cluster into one richer
# "super-lesson" via a local claude_session pass, grounded in the source lessons
# (INV-AH) with a re-embedding DRIFT_FLOOR guard. Opus by default — chair-facing
# quality. The synthesised set is smaller, so the writer's limit=15 stops truncating
# (the real fix for the silent cap, #157).
LESSON_SYNTH_MODEL = os.environ.get("LESSON_SYNTH_MODEL", HALACHA_EXTRACT_MODEL)
LESSON_SYNTH_EFFORT = os.environ.get("LESSON_SYNTH_EFFORT", "high")
LESSON_SYNTH_DRIFT_FLOOR = float(os.environ.get("LESSON_SYNTH_DRIFT_FLOOR", "0.80"))
# Cosine floor for two lessons to land in the same cluster (greedy, within shard).
LESSON_SYNTH_CLUSTER_THRESHOLD = float(os.environ.get("LESSON_SYNTH_CLUSTER_THRESHOLD", "0.82"))
# Mistral OCR (fallback for scanned PDFs — replaces Google Cloud Vision)
MISTRAL_API_KEY = os.environ.get("MISTRAL_API_KEY", "")