Merge pull request 'fix(learning): chair_name במקור — סופי-ועדה תמיד נכנס לקורפוס-הפסיקה (#134)' (#226) from worktree-chair-name-rootfix into main
This commit was merged in pull request #226.
This commit is contained in:
@@ -362,3 +362,34 @@ def parse_llm_json(raw: str):
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
return None
|
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)
|
||||||
|
|||||||
@@ -1555,22 +1555,30 @@ async def create_case(
|
|||||||
practice_area: str = "",
|
practice_area: str = "",
|
||||||
appeal_subtype: str = "",
|
appeal_subtype: str = "",
|
||||||
proceeding_type: str = "ערר",
|
proceeding_type: str = "ערר",
|
||||||
|
# Default "" — resolved below to the committee chair (never stored empty).
|
||||||
|
# internal_committee corpus copies of this case's final REQUIRE a chair
|
||||||
|
# (DB constraint case_law_internal_chair_check); setting it at creation
|
||||||
|
# (INV-G1, source) keeps the learning loop's precedent copy from failing.
|
||||||
|
chair_name: str = "",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
case_id = uuid4()
|
case_id = uuid4()
|
||||||
|
canonical_number = _canonical_case_number(case_number)
|
||||||
|
resolved_chair = config.committee_chair_for_case(
|
||||||
|
{"chair_name": chair_name}, canonical_number)
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
"""INSERT INTO cases (id, case_number, title, appellants, respondents,
|
"""INSERT INTO cases (id, case_number, title, appellants, respondents,
|
||||||
subject, property_address, permit_number, committee_type,
|
subject, property_address, permit_number, committee_type,
|
||||||
hearing_date, notes, expected_outcome,
|
hearing_date, notes, expected_outcome,
|
||||||
practice_area, appeal_subtype, proceeding_type)
|
practice_area, appeal_subtype, proceeding_type, chair_name)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)""",
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)""",
|
||||||
case_id, _canonical_case_number(case_number), title,
|
case_id, canonical_number, title,
|
||||||
json.dumps(appellants or []),
|
json.dumps(appellants or []),
|
||||||
json.dumps(respondents or []),
|
json.dumps(respondents or []),
|
||||||
subject, property_address, permit_number, committee_type,
|
subject, property_address, permit_number, committee_type,
|
||||||
hearing_date, notes, expected_outcome,
|
hearing_date, notes, expected_outcome,
|
||||||
practice_area, appeal_subtype, proceeding_type,
|
practice_area, appeal_subtype, proceeding_type, resolved_chair,
|
||||||
)
|
)
|
||||||
return await get_case(case_id)
|
return await get_case(case_id)
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ async def case_create(
|
|||||||
practice_area: str = "",
|
practice_area: str = "",
|
||||||
appeal_subtype: str = "",
|
appeal_subtype: str = "",
|
||||||
proceeding_type: str = "",
|
proceeding_type: str = "",
|
||||||
|
chair_name: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""יצירת תיק ערר חדש.
|
"""יצירת תיק ערר חדש.
|
||||||
|
|
||||||
@@ -153,6 +154,9 @@ async def case_create(
|
|||||||
appeal_subtype: סוג ערר (building_permit / betterment_levy / compensation_197).
|
appeal_subtype: סוג ערר (building_permit / betterment_levy / compensation_197).
|
||||||
ריק = יוסק אוטומטית ממספר התיק
|
ריק = יוסק אוטומטית ממספר התיק
|
||||||
proceeding_type: 'ערר' / 'בל"מ'. ריק = יוסק מ-appeal_subtype/subject.
|
proceeding_type: 'ערר' / 'בל"מ'. ריק = יוסק מ-appeal_subtype/subject.
|
||||||
|
chair_name: שם יו"ר הוועדה. ריק = ברירת-המחדל של הוועדה לפי תחילית
|
||||||
|
מספר-התיק (SoT: config.committee_chair_for_case) — נשמר
|
||||||
|
תמיד לא-ריק כדי שהעתק-הסופי לקורפוס-הפסיקה לא ייכשל.
|
||||||
"""
|
"""
|
||||||
# INV-TOOL3 / GAP-52: idempotent on case_number (already UNIQUE in schema).
|
# INV-TOOL3 / GAP-52: idempotent on case_number (already UNIQUE in schema).
|
||||||
# Re-creating an existing case returns it instead of raising a unique-violation.
|
# Re-creating an existing case returns it instead of raising a unique-violation.
|
||||||
@@ -204,6 +208,7 @@ async def case_create(
|
|||||||
practice_area=practice_area,
|
practice_area=practice_area,
|
||||||
appeal_subtype=appeal_subtype,
|
appeal_subtype=appeal_subtype,
|
||||||
proceeding_type=resolved_proc,
|
proceeding_type=resolved_proc,
|
||||||
|
chair_name=chair_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
# If the user overrode the case-number convention (e.g. case 8500 marked
|
# If the user overrode the case-number convention (e.g. case 8500 marked
|
||||||
|
|||||||
@@ -326,13 +326,20 @@ async def ingest_final_version(
|
|||||||
return err(str(e))
|
return err(str(e))
|
||||||
|
|
||||||
# Auto-ingest into internal committee decisions corpus (best-effort).
|
# Auto-ingest into internal committee decisions corpus (best-effort).
|
||||||
|
# chair_name is resolved via the shared SoT (config.committee_chair_for_case)
|
||||||
|
# — the SAME resolver the FastAPI upload path uses — so the two paths cannot
|
||||||
|
# drift (INV-G2) and the DB chair constraint is never hit on an empty chair
|
||||||
|
# (INV-G1: chair normalised at source). Failures are surfaced, not swallowed
|
||||||
|
# (engineering rule §6 / feedback_silent_swallow): the result carries the
|
||||||
|
# reason and final_learning_pipeline prints it.
|
||||||
try:
|
try:
|
||||||
|
from legal_mcp import config
|
||||||
from legal_mcp.services import internal_decisions as int_svc
|
from legal_mcp.services import internal_decisions as int_svc
|
||||||
await int_svc.ingest_internal_decision(
|
await int_svc.ingest_internal_decision(
|
||||||
case_number=case_number,
|
case_number=case_number,
|
||||||
case_name=case.get("title", ""),
|
case_name=case.get("title", ""),
|
||||||
decision_date=case.get("decision_date"),
|
decision_date=case.get("decision_date"),
|
||||||
chair_name=case.get("chair_name", ""),
|
chair_name=config.committee_chair_for_case(case, case_number),
|
||||||
district="ירושלים",
|
district="ירושלים",
|
||||||
practice_area=case.get("practice_area", ""),
|
practice_area=case.get("practice_area", ""),
|
||||||
appeal_subtype=case.get("appeal_subtype", ""),
|
appeal_subtype=case.get("appeal_subtype", ""),
|
||||||
@@ -340,8 +347,10 @@ async def ingest_final_version(
|
|||||||
)
|
)
|
||||||
result["internal_corpus_ingested"] = True
|
result["internal_corpus_ingested"] = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("ingest_final_version: internal corpus ingestion failed (non-fatal): %s", e)
|
logger.warning(
|
||||||
|
"ingest_final_version: internal corpus ingestion failed (non-fatal): %s", e)
|
||||||
result["internal_corpus_ingested"] = False
|
result["internal_corpus_ingested"] = False
|
||||||
|
result["internal_corpus_error"] = str(e)
|
||||||
|
|
||||||
return ok(result)
|
return ok(result)
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
| `halacha_rule_role_backfill.py` | python | **INV-DM7** — backfill חד-פעמי: מסווג-מחדש את ההלכות הישנות (`rule_type IN ('binding','persuasive')` — ערכי-סמכות שנשמרו במסווה תפקיד לפני פיצול הצירים) לאחד מחמשת **תפקידי-הכלל** (holding/interpretive/procedural/application/obiter) דרך claude_session המקומי (אפס עלות). **לא נוגע בסמכות** (נגזרת מ-`precedent_level`). `--apply` (ברירת-מחדל dry-run) / `--limit N` / `--concurrency`. כותב backup CSV ל-`data/audit/` תחילה. fail-safe (פריט שנכשל → נשמר ערך ישן). **חובה מקומי** (claude_session). | ידני חד-פעמי אחרי deploy של פיצול-הסמכות |
|
| `halacha_rule_role_backfill.py` | python | **INV-DM7** — backfill חד-פעמי: מסווג-מחדש את ההלכות הישנות (`rule_type IN ('binding','persuasive')` — ערכי-סמכות שנשמרו במסווה תפקיד לפני פיצול הצירים) לאחד מחמשת **תפקידי-הכלל** (holding/interpretive/procedural/application/obiter) דרך claude_session המקומי (אפס עלות). **לא נוגע בסמכות** (נגזרת מ-`precedent_level`). `--apply` (ברירת-מחדל dry-run) / `--limit N` / `--concurrency`. כותב backup CSV ל-`data/audit/` תחילה. fail-safe (פריט שנכשל → נשמר ערך ישן). **חובה מקומי** (claude_session). | ידני חד-פעמי אחרי deploy של פיצול-הסמכות |
|
||||||
| `halacha_batch_reconcile.py` | python | **#82.7** — dedup חוצה-פסקים offline (שמרני, **dry-run בלבד**). dedup-on-insert משווה רק תוך-פסק; כאן סף מחמיר (cosine ≥0.95, `--cosine`) ולא-הרסני: מאתר זוגות הלכות near-duplicate בין פסקים שונים (pgvector `<=>` exact) עם איתות לקסיקלי (Jaccard/Levenshtein) ומדווח ל-CSV ב-`data/audit/` לסקירת היו"ר. לא מדלג/ממזג/מוחק. `--include-pending`. **`--link`** רושם את הזוגות שנמצאו כ-`equivalent_halachot` (parallel authority, #84.2 — קישור-מקביל ברמת-הלכה, **לא** ציטוט; idempotent, לא-הרסני). רץ עם venv של mcp-server. אומת: 800 הלכות → 5 זוגות (קושרו). | ידני — דוח-סקירה / `--link` לקישור |
|
| `halacha_batch_reconcile.py` | python | **#82.7** — dedup חוצה-פסקים offline (שמרני, **dry-run בלבד**). dedup-on-insert משווה רק תוך-פסק; כאן סף מחמיר (cosine ≥0.95, `--cosine`) ולא-הרסני: מאתר זוגות הלכות near-duplicate בין פסקים שונים (pgvector `<=>` exact) עם איתות לקסיקלי (Jaccard/Levenshtein) ומדווח ל-CSV ב-`data/audit/` לסקירת היו"ר. לא מדלג/ממזג/מוחק. `--include-pending`. **`--link`** רושם את הזוגות שנמצאו כ-`equivalent_halachot` (parallel authority, #84.2 — קישור-מקביל ברמת-הלכה, **לא** ציטוט; idempotent, לא-הרסני). רץ עם venv של mcp-server. אומת: 800 הלכות → 5 זוגות (קושרו). | ידני — דוח-סקירה / `--link` לקישור |
|
||||||
| `calibrate_halacha_dedup.py` | python | **#82.1** — כיול ספי ה-dedup הלקסיקלי (#82.3) מול gold-set הניקוי. קורא `halacha-cleanup-manifest-*.csv` (זוגות duplicate↔survivor מתויגי-אדם), טוען טקסט-survivor מה-DB, ו-sweep של (jaccard_min × levenshtein_min) עם P/R/F1, מסמן את נקודת-העבודה המוגדרת. אימת ש-(0.55, 0.70) → **precision 1.0** (אפס false-merge), recall 0.30 — מתאים לאיתות-משני שחוסם auto-approve. `--manifest <path>`. רץ עם venv של mcp-server | חד-פעמי — כיול (בוצע 2026-06-06) |
|
| `calibrate_halacha_dedup.py` | python | **#82.1** — כיול ספי ה-dedup הלקסיקלי (#82.3) מול gold-set הניקוי. קורא `halacha-cleanup-manifest-*.csv` (זוגות duplicate↔survivor מתויגי-אדם), טוען טקסט-survivor מה-DB, ו-sweep של (jaccard_min × levenshtein_min) עם P/R/F1, מסמן את נקודת-העבודה המוגדרת. אימת ש-(0.55, 0.70) → **precision 1.0** (אפס false-merge), recall 0.30 — מתאים לאיתות-משני שחוסם auto-approve. `--manifest <path>`. רץ עם venv של mcp-server | חד-פעמי — כיול (בוצע 2026-06-06) |
|
||||||
| `audit_corpus_integrity.py` | python | בדיקה תקופתית של עקביות הקורפוס — 3 בדיקות SQL read-only על `case_law` ו-`cases`: (A) `external_upload` עם prefix פנימי `ערר`/`בל"מ`; (B) `internal_committee` חסר `chair_name`/`district`; (C) `cases.practice_area` מחוץ ל-{`rishuy_uvniya`, `betterment_levy`, `compensation_197`, `''`}. כותב log מצטבר ל-`data/logs/corpus_integrity_audit.log` ובמצב הפרות שולח wakeup ל-CEO ב-Paperclip (best-effort, רק אם `PAPERCLIP_API_URL`+`PAPERCLIP_API_KEY` מוגדרים). דגל: `--no-notify`. Idempotent, יוצא 0. **Cron יומי 07:00**: `0 7 * * * /home/chaim/legal-ai/mcp-server/.venv/bin/python /home/chaim/legal-ai/scripts/audit_corpus_integrity.py` | `0 7 * * *` (cron) |
|
| `audit_corpus_integrity.py` | python | בדיקה תקופתית של עקביות הקורפוס — 5 בדיקות SQL read-only על `case_law` ו-`cases`: (A) `external_upload` עם prefix פנימי `ערר`/`בל"מ`; (B) `internal_committee` חסר `chair_name`/`district`; (C) `cases.practice_area` מחוץ ל-{`rishuy_uvniya`, `betterment_levy`, `compensation_197`, `''`}; (D) תיקים מוכרעים (`final`/`exported`/`reviewed`) ללא `chair_name` (chair ריק מפיל בשקט את העתק-הסופי לקורפוס-הפסיקה — INV-G1); (E) תיקי `final` חתומים שחסרים מקורפוס-הפסיקה הפנימי (`internal_committee`). כותב log מצטבר ל-`data/logs/corpus_integrity_audit.log` ובמצב הפרות שולח wakeup ל-CEO ב-Paperclip (best-effort, רק אם `PAPERCLIP_API_URL`+`PAPERCLIP_API_KEY` מוגדרים). דגל: `--no-notify`. Idempotent, יוצא 0. **Cron יומי 07:00**: `0 7 * * * /home/chaim/legal-ai/mcp-server/.venv/bin/python /home/chaim/legal-ai/scripts/audit_corpus_integrity.py` | `0 7 * * *` (cron) |
|
||||||
| `backfill_legal_arguments.py` | python | Backfill `legal_arguments` לתיקים עם `claims` קיימים (TaskMaster #36). מקבץ פרופוזיציות גולמיות לטיעונים משפטיים מובחנים (~6-12 לכל צד) דרך `argument_aggregator.aggregate_claims_to_arguments` (Claude CLI). תומך `--dry-run`/`--apply`/`--force`/`--case <num>...`. **חייב לרוץ מהמכונה המקומית** (לא קונטיינר) — `claude_session` דורש Claude CLI | ידני per-case (`python scripts/backfill_legal_arguments.py --apply --case 1017-03-26`) |
|
| `backfill_legal_arguments.py` | python | Backfill `legal_arguments` לתיקים עם `claims` קיימים (TaskMaster #36). מקבץ פרופוזיציות גולמיות לטיעונים משפטיים מובחנים (~6-12 לכל צד) דרך `argument_aggregator.aggregate_claims_to_arguments` (Claude CLI). תומך `--dry-run`/`--apply`/`--force`/`--case <num>...`. **חייב לרוץ מהמכונה המקומית** (לא קונטיינר) — `claude_session` דורש Claude CLI | ידני per-case (`python scripts/backfill_legal_arguments.py --apply --case 1017-03-26`) |
|
||||||
| `upload_blam_decisions.py` | python | חד-פעמי (2026-05-26) — העלאת 2 החלטות בל"מ ל-`case_law` (8126/24 סופר נוח, 8047/23 הרנון) דרך `ingest_internal_decision` ישיר, עוקף MCP server שטרם נטען מחדש אחרי הוספת `proceeding_type`. **לא להריץ שוב** | חד-פעמי — להעביר ל-`.archive/` בהזדמנות |
|
| `upload_blam_decisions.py` | python | חד-פעמי (2026-05-26) — העלאת 2 החלטות בל"מ ל-`case_law` (8126/24 סופר נוח, 8047/23 הרנון) דרך `ingest_internal_decision` ישיר, עוקף MCP server שטרם נטען מחדש אחרי הוספת `proceeding_type`. **לא להריץ שוב** | חד-פעמי — להעביר ל-`.archive/` בהזדמנות |
|
||||||
| `process_pending_blam.py` | python | חד-פעמי (2026-05-26) — הרצת metadata + halacha extraction על 2 החלטות בל"מ שעלו ב-`upload_blam_decisions.py`. עוקף MCP (אותו טעם). **לא להריץ שוב** | חד-פעמי — להעביר ל-`.archive/` בהזדמנות |
|
| `process_pending_blam.py` | python | חד-פעמי (2026-05-26) — הרצת metadata + halacha extraction על 2 החלטות בל"מ שעלו ב-`upload_blam_decisions.py`. עוקף MCP (אותו טעם). **לא להריץ שוב** | חד-פעמי — להעביר ל-`.archive/` בהזדמנות |
|
||||||
|
|||||||
@@ -82,6 +82,28 @@ CHECK_C_SQL = (
|
|||||||
" 'compensation_197', '') "
|
" 'compensation_197', '') "
|
||||||
"ORDER BY case_number"
|
"ORDER BY case_number"
|
||||||
)
|
)
|
||||||
|
# D. cases that reached a decided state but have no chair_name. An empty chair
|
||||||
|
# silently breaks the internal_committee corpus copy of the final
|
||||||
|
# (case_law_internal_chair_check) — chair must be set at source (INV-G1).
|
||||||
|
CHECK_D_SQL = (
|
||||||
|
"SELECT id, case_number, status FROM cases "
|
||||||
|
"WHERE status IN ('final', 'exported', 'reviewed') "
|
||||||
|
"AND (chair_name IS NULL OR chair_name = '') "
|
||||||
|
"ORDER BY case_number"
|
||||||
|
)
|
||||||
|
# E. SIGNED finals that never landed in the citable precedent corpus
|
||||||
|
# (case_law, source_kind='internal_committee'). Only status='final' means the
|
||||||
|
# chair's signed decision was ingested — 'exported' is merely OUR draft DOCX
|
||||||
|
# and legitimately has no precedent copy. This is the exact failure the
|
||||||
|
# chair_name fix prevents going forward; the check catches any regression.
|
||||||
|
CHECK_E_SQL = (
|
||||||
|
"SELECT c.id, c.case_number, c.status FROM cases c "
|
||||||
|
"WHERE c.status = 'final' "
|
||||||
|
"AND NOT EXISTS (SELECT 1 FROM case_law cl "
|
||||||
|
" WHERE cl.case_number = c.case_number "
|
||||||
|
" AND cl.source_kind = 'internal_committee') "
|
||||||
|
"ORDER BY c.case_number"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -178,6 +200,8 @@ def _format_report(
|
|||||||
a_hits: list[dict],
|
a_hits: list[dict],
|
||||||
b_hits: list[dict],
|
b_hits: list[dict],
|
||||||
c_hits: list[dict],
|
c_hits: list[dict],
|
||||||
|
d_hits: list[dict],
|
||||||
|
e_hits: list[dict],
|
||||||
ts: datetime,
|
ts: datetime,
|
||||||
) -> str:
|
) -> str:
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
@@ -215,6 +239,29 @@ def _format_report(
|
|||||||
if len(c_hits) > 50:
|
if len(c_hits) > 50:
|
||||||
parts.append(f" ... ({len(c_hits) - 50} more truncated)")
|
parts.append(f" ... ({len(c_hits) - 50} more truncated)")
|
||||||
parts.append("")
|
parts.append("")
|
||||||
|
parts.append(
|
||||||
|
f"Check D (decided cases missing chair_name): {len(d_hits)} hit(s)"
|
||||||
|
)
|
||||||
|
for row in d_hits[:50]:
|
||||||
|
parts.append(
|
||||||
|
f" - id={row['id']} case_number={row['case_number']!r} "
|
||||||
|
f"status={row.get('status')!r}"
|
||||||
|
)
|
||||||
|
if len(d_hits) > 50:
|
||||||
|
parts.append(f" ... ({len(d_hits) - 50} more truncated)")
|
||||||
|
parts.append("")
|
||||||
|
parts.append(
|
||||||
|
f"Check E (signed-final cases missing from internal_committee "
|
||||||
|
f"precedent corpus): {len(e_hits)} hit(s)"
|
||||||
|
)
|
||||||
|
for row in e_hits[:50]:
|
||||||
|
parts.append(
|
||||||
|
f" - id={row['id']} case_number={row['case_number']!r} "
|
||||||
|
f"status={row.get('status')!r}"
|
||||||
|
)
|
||||||
|
if len(e_hits) > 50:
|
||||||
|
parts.append(f" ... ({len(e_hits) - 50} more truncated)")
|
||||||
|
parts.append("")
|
||||||
return "\n".join(parts)
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
@@ -225,12 +272,14 @@ async def main(args: argparse.Namespace) -> int:
|
|||||||
a_hits = await _run_check(conn, CHECK_A_SQL)
|
a_hits = await _run_check(conn, CHECK_A_SQL)
|
||||||
b_hits = await _run_check(conn, CHECK_B_SQL)
|
b_hits = await _run_check(conn, CHECK_B_SQL)
|
||||||
c_hits = await _run_check(conn, CHECK_C_SQL)
|
c_hits = await _run_check(conn, CHECK_C_SQL)
|
||||||
|
d_hits = await _run_check(conn, CHECK_D_SQL)
|
||||||
|
e_hits = await _run_check(conn, CHECK_E_SQL)
|
||||||
finally:
|
finally:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
total = len(a_hits) + len(b_hits) + len(c_hits)
|
total = len(a_hits) + len(b_hits) + len(c_hits) + len(d_hits) + len(e_hits)
|
||||||
ts = datetime.now(timezone.utc)
|
ts = datetime.now(timezone.utc)
|
||||||
report = _format_report(a_hits, b_hits, c_hits, ts)
|
report = _format_report(a_hits, b_hits, c_hits, d_hits, e_hits, ts)
|
||||||
|
|
||||||
# Always write to log (creates dir + file if missing).
|
# Always write to log (creates dir + file if missing).
|
||||||
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -246,8 +295,8 @@ async def main(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"found %d total violation(s) (A=%d, B=%d, C=%d)",
|
"found %d total violation(s) (A=%d, B=%d, C=%d, D=%d, E=%d)",
|
||||||
total, len(a_hits), len(b_hits), len(c_hits),
|
total, len(a_hits), len(b_hits), len(c_hits), len(d_hits), len(e_hits),
|
||||||
)
|
)
|
||||||
|
|
||||||
if args.notify:
|
if args.notify:
|
||||||
@@ -256,6 +305,8 @@ async def main(args: argparse.Namespace) -> int:
|
|||||||
f"- Check A (external_upload עם prefix פנימי): {len(a_hits)}",
|
f"- Check A (external_upload עם prefix פנימי): {len(a_hits)}",
|
||||||
f"- Check B (internal_committee חסר chair/district): {len(b_hits)}",
|
f"- Check B (internal_committee חסר chair/district): {len(b_hits)}",
|
||||||
f"- Check C (cases.practice_area לא תקין): {len(c_hits)}",
|
f"- Check C (cases.practice_area לא תקין): {len(c_hits)}",
|
||||||
|
f"- Check D (תיקים מוכרעים ללא chair_name): {len(d_hits)}",
|
||||||
|
f"- Check E (סופיים חסרים מקורפוס-הפסיקה הפנימי): {len(e_hits)}",
|
||||||
"",
|
"",
|
||||||
f"פירוט מלא: {LOG_PATH}",
|
f"פירוט מלא: {LOG_PATH}",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -112,6 +112,11 @@ async def main(args: argparse.Namespace) -> int:
|
|||||||
ds = d.get("diff_stats", {})
|
ds = d.get("diff_stats", {})
|
||||||
print(f" ✓ change {ds.get('change_percent')}% · lessons {d.get('lessons_count')} "
|
print(f" ✓ change {ds.get('change_percent')}% · lessons {d.get('lessons_count')} "
|
||||||
f"· new_expr {d.get('new_expressions')}")
|
f"· new_expr {d.get('new_expressions')}")
|
||||||
|
# Surface (do not swallow) a failed precedent-corpus copy so the final
|
||||||
|
# does not silently miss the citable internal_committee library.
|
||||||
|
if d.get("internal_corpus_ingested") is False:
|
||||||
|
print(f" ⚠️ קורפוס-פסיקה: ההעתק הפנימי (internal_committee) לא נוצר — "
|
||||||
|
f"{d.get('internal_corpus_error', 'סיבה לא ידועה')}", flush=True)
|
||||||
return {"ingest": "done"}
|
return {"ingest": "done"}
|
||||||
|
|
||||||
async def step_enroll(results: dict) -> dict:
|
async def step_enroll(results: dict) -> dict:
|
||||||
|
|||||||
18
web/app.py
18
web/app.py
@@ -3492,20 +3492,10 @@ async def api_mark_final(case_number: str, filename: str):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Our committee's standing chair — internal_committee rows REQUIRE a chair_name
|
# Committee chair resolution lives in ONE place — config.committee_chair_for_case
|
||||||
# (DB constraint case_law_internal_chair_check). Both CMP (1xxx) and CMPA (8/9xxx)
|
# (INV-G2: the FastAPI upload path and the MCP learning path must not drift into
|
||||||
# are currently chaired by Dafna Tamir; map by prefix so adding a chair later is local.
|
# parallel chair logic). Thin alias keeps call-sites here readable.
|
||||||
COMMITTEE_CHAIR_BY_PREFIX = {"1": "דפנה תמיר", "8": "דפנה תמיר", "9": "דפנה תמיר"}
|
_committee_chair_for_case = config.committee_chair_for_case
|
||||||
COMMITTEE_CHAIR_DEFAULT = "דפנה תמיר"
|
|
||||||
|
|
||||||
|
|
||||||
def _committee_chair_for_case(case: dict, 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."""
|
|
||||||
existing = (case.get("chair_name") or "").strip()
|
|
||||||
if existing:
|
|
||||||
return existing
|
|
||||||
return COMMITTEE_CHAIR_BY_PREFIX.get(case_number[:1], COMMITTEE_CHAIR_DEFAULT)
|
|
||||||
|
|
||||||
|
|
||||||
def _party_name(parties) -> str:
|
def _party_name(parties) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user