feat(proceeding-type): explicit ערר/בל"מ field for cases + corpus
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m40s

Same case_number can exist as both a regular appeal (ערר) and an
extension-of-time request (בל"מ), and we were inferring the difference
from appeal_subtype prefixes — fragile, and case-number lookups
weren't disambiguated. Now stored as a first-class field on both
case_law (corpus) and cases (live cases), with partial unique indexes
on (case_number, proceeding_type).

- SCHEMA_V15: column + CHECK constraints + backfill from
  appeal_subtype LIKE 'extension_request_%' + partial unique indexes
  replace the old global UNIQUE(case_number).
- derive_proceeding_type() centralizes the inference rule
  (extension_request_* → בל"מ; subject regex fallback; default ערר).
- Metadata extractor prompt asks Claude to populate the new field
  explicitly; apply_to_record writes it for internal_committee rows.
- internal_decision_upload, case_create, case_update accept an
  optional proceeding_type; FastAPI request models expose it.
- Wizard + edit dialog get a sided Select; case header renders the
  resolved label (ערר / בל"מ).
- Uploaded the 2 staged בל"מ decisions on betterment levy:
  8126/24 (סופר נוח, 13 chunks), 8047/23 (הרנון, 48 chunks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 09:17:33 +00:00
parent 1645653ba9
commit d359ab9884
15 changed files with 308 additions and 14 deletions

View File

@@ -130,6 +130,7 @@ async def case_create(
expected_outcome: str = "",
practice_area: str = "",
appeal_subtype: str = "",
proceeding_type: str = "",
) -> str:
"""יצירת תיק ערר חדש.
@@ -150,6 +151,7 @@ async def case_create(
אוטומטית ממספר התיק (1xxx→רישוי, 8xxx→השבחה, 9xxx→197)
appeal_subtype: סוג ערר (building_permit / betterment_levy / compensation_197).
ריק = יוסק אוטומטית ממספר התיק
proceeding_type: 'ערר' / 'בל"מ'. ריק = יוסק מ-appeal_subtype/subject.
"""
from datetime import date as date_type
@@ -173,6 +175,11 @@ async def case_create(
appeal_subtype = derived_subtype
pa.validate(practice_area, appeal_subtype)
# proceeding_type: explicit override > derived from subtype/subject > 'ערר'
resolved_proc = proceeding_type.strip() or pa.derive_proceeding_type(
appeal_subtype=appeal_subtype, subject=subject,
)
case = await db.create_case(
case_number=case_number,
title=title,
@@ -187,6 +194,7 @@ async def case_create(
expected_outcome=expected_outcome,
practice_area=practice_area,
appeal_subtype=appeal_subtype,
proceeding_type=resolved_proc,
)
# If the user overrode the case-number convention (e.g. case 8500 marked
@@ -290,6 +298,7 @@ async def case_update(
respondents: list[str] | None = None,
property_address: str = "",
permit_number: str = "",
proceeding_type: str = "",
) -> str:
"""עדכון פרטי תיק.
@@ -307,6 +316,7 @@ async def case_update(
respondents: רשימת משיבים חדשה
property_address: כתובת נכס חדשה
permit_number: מספר תכנית/בקשה חדש
proceeding_type: 'ערר' / 'בל"מ' — ריק = ללא שינוי
"""
from datetime import date as date_type
@@ -359,6 +369,12 @@ async def case_update(
fields["property_address"] = property_address
if permit_number:
fields["permit_number"] = permit_number
if proceeding_type:
if proceeding_type not in {"ערר", 'בל"מ'}:
raise ValueError(
f"proceeding_type לא תקין: {proceeding_type!r}. ערכים תקפים: ערר / בל\"מ"
)
fields["proceeding_type"] = proceeding_type
updated = await db.update_case(UUID(case["id"]), **fields)

View File

@@ -21,6 +21,10 @@ from legal_mcp.services import internal_decisions as int_svc
# Valid Hebrew district names (matches _COURT_TO_DISTRICT in service)
VALID_DISTRICTS = {"ירושלים", "מרכז", "תל אביב", "תל-אביב", "צפון", "דרום", "חיפה", "ארצי"}
# proceeding_type — ערר vs בל"מ. The service can derive it from
# appeal_subtype/subject if left empty, so this stays optional at the API.
VALID_PROCEEDING_TYPES = {"ערר", 'בל"מ'}
def _ok(payload) -> str:
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
@@ -43,6 +47,7 @@ async def internal_decision_upload(
subject_tags: list[str] | None = None,
summary: str = "",
is_binding: bool = False,
proceeding_type: str = "",
) -> str:
"""העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.
@@ -62,6 +67,7 @@ async def internal_decision_upload(
appeal_subtype: building_permit / וכו'.
subject_tags: תגיות נושא.
is_binding: בד"כ False (ועדת ערר לא מחייבת ועדה אחרת — שכנוע אופקי).
proceeding_type: 'ערר' או 'בל"מ'. אם ריק — נגזר מ-appeal_subtype/case_name.
Returns: JSON עם case_law_id, מספר chunks, halachot_pending.
"""
@@ -83,6 +89,11 @@ async def internal_decision_upload(
f"district לא תקין: {district!r}. ערכים תקפים: "
+ ", ".join(sorted(VALID_DISTRICTS))
)
if proceeding_type.strip() and proceeding_type.strip() not in VALID_PROCEEDING_TYPES:
return _err(
f"proceeding_type לא תקין: {proceeding_type!r}. ערכים תקפים: "
+ ", ".join(sorted(VALID_PROCEEDING_TYPES))
)
try:
result = await int_svc.ingest_internal_decision(
@@ -98,6 +109,7 @@ async def internal_decision_upload(
summary=summary,
is_binding=is_binding,
file_path=file_path,
proceeding_type=proceeding_type,
)
except Exception as e:
return _err(str(e))