fix(learning): chair_name במקור — סופי-ועדה תמיד נכנס לקורפוס-הפסיקה (TaskMaster #134)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s

הבאג: שלב-הלמידה (ingest_final_version → ingest_internal_decision) מוסיף כל
סופי כתקדים ציטוטי ב-case_law (source_kind=internal_committee), אך נכשל
בשקט (non-fatal warning) כש-cases.chair_name ריק — בגלל constraint
case_law_internal_chair_check. כך סופיים של 1194/1200/8070 לא נכנסו
לקורפוס-הפסיקה. שורש: (1) chair_name לא נקבע בפתיחת תיק; (2) מסלול-ה-MCP
העביר chair גולמי בעוד מסלול-ה-UI (web/) כבר פתר אותו דטרמיניסטית —
**מסלולים מקבילים מתפצלים (הפרת INV-G2)**; (3) הכשל נבלע (נגד §6).

תיקון-שורש (3 שכבות):
1. **SoT יחיד (INV-G2):** `config.committee_chair_for_case` — המקום היחיד
   שגם web/app.py וגם tools/workflow.py + db.create_case גוזרים ממנו chair
   (לפי תחילית מספר-התיק; override ל-env). web/ אחוד אליו (הוסרה הכפילות).
2. **נרמול-במקור (INV-G1):** `db.create_case` קובע chair_name תמיד לא-ריק;
   `cases.case_create` חושף param. `ingest_final_version` גוזר chair מה-SoT
   במקום הערך הגולמי → ה-constraint לא נופל.
3. **נראות (§6/feedback_silent_swallow):** כשל-העתק מוחזר ב-result
   (`internal_corpus_error`) ו-`final_learning_pipeline` מדפיס אזהרה — לא
   נבלע. backfill ל-11 תיקים עם chair ריק. `audit_corpus_integrity`:
   נוספו CHECK_D (תיקים מוכרעים ללא chair) + CHECK_E (סופי-final חסר
   מקורפוס-הפסיקה) — שניהם 0 כעת.

invariants: מקיים INV-G1 (נרמול בכתיבה), INV-G2 (מסלול-יחיד, אוחד web↔MCP),
§6 (אין בליעה שקטה). בדיקות: py_compile + 14 pytest (chair_seed_gate,
audit_provenance) + integration של create_case (default+override) + הרצת
ה-audit החי (A–E=0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 07:25:54 +00:00
parent 412bd091cf
commit 242e6cfd11
8 changed files with 124 additions and 25 deletions

View File

@@ -362,3 +362,34 @@ def parse_llm_json(raw: str):
except json.JSONDecodeError:
pass
return None
# ── Committee chair — single source of truth (INV-G2) ─────────────────
# internal_committee rows REQUIRE a non-empty chair_name (DB constraint
# case_law_internal_chair_check). Our committee (CMP 1xxx, CMPA 8/9xxx) is
# chaired by Dafna Tamir; map by case-number prefix so adding a future chair
# stays a one-line local change. This resolver is the ONE place both the
# FastAPI final-upload path (web/app.py) and the MCP learning path
# (tools/workflow.py + services/db.create_case) derive the chair from — so
# the two cannot drift into parallel logic. Override via env for another
# committee.
COMMITTEE_CHAIR_DEFAULT = os.environ.get("DEFAULT_CHAIR_NAME", "דפנה תמיר")
COMMITTEE_CHAIR_BY_PREFIX = {
"1": COMMITTEE_CHAIR_DEFAULT,
"8": COMMITTEE_CHAIR_DEFAULT,
"9": COMMITTEE_CHAIR_DEFAULT,
}
def committee_chair_for_case(case: dict | None, case_number: str) -> str:
"""Resolve the chair for one of OUR decisions deterministically (no LLM):
the case's own chair_name, else the committee default by case-number prefix.
Never returns empty for a valid case number — this is how chair_name is
normalised at the source (INV-G1) so internal_committee corpus copies of
finals never silently fail the DB chair constraint.
"""
existing = ((case or {}).get("chair_name") or "").strip()
if existing:
return existing
return COMMITTEE_CHAIR_BY_PREFIX.get((case_number or "")[:1], COMMITTEE_CHAIR_DEFAULT)