feat(proceeding-type): explicit ערר/בל"מ field for cases + corpus
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m40s
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:
@@ -124,6 +124,7 @@ mcp__legal-ai__internal_decision_upload(
|
|||||||
decision_date="2020-11-15",
|
decision_date="2020-11-15",
|
||||||
practice_area="rishuy_uvniya", # Axis B
|
practice_area="rishuy_uvniya", # Axis B
|
||||||
appeal_subtype="building_permit",
|
appeal_subtype="building_permit",
|
||||||
|
proceeding_type="ערר", # 'ערר' / 'בל"מ' — ראה מטה
|
||||||
subject_tags=["שימוש חורג"],
|
subject_tags=["שימוש חורג"],
|
||||||
is_binding=False, # תמיד False — שכנוע אופקי, לא חוב
|
is_binding=False, # תמיד False — שכנוע אופקי, לא חוב
|
||||||
)
|
)
|
||||||
@@ -135,6 +136,13 @@ mcp__legal-ai__internal_decision_upload(
|
|||||||
- `chair_name` — בלעדיו אי-אפשר לחפש סלקטיבית לפי הרכב
|
- `chair_name` — בלעדיו אי-אפשר לחפש סלקטיבית לפי הרכב
|
||||||
- `district` — ערכים תקפים: **ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי** (גם "תל-אביב" עם מקף נקלט)
|
- `district` — ערכים תקפים: **ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי** (גם "תל-אביב" עם מקף נקלט)
|
||||||
|
|
||||||
|
**שדה מומלץ — `proceeding_type`:**
|
||||||
|
- `"ערר"` — הליך ערר עיקרי (כותרת ב-PDF: "ערר (ועדות ערר ...) NNNN/YY")
|
||||||
|
- `'בל"מ'` — בקשה להארכת מועד להגשת ערר (כותרת: "בל\"מ NNNN/YY" או נושא "בקשה להארכת מועד להגשת ערר")
|
||||||
|
- שני הסוגים יכולים לחלוק אותו מספר תיק (למשל 8047/23 קיים גם כערר וגם כבל"מ).
|
||||||
|
- בכותרת הראשית של ה-PDF זה תמיד מפורש — לקרוא משם ולא לנחש.
|
||||||
|
- אם תשאיר ריק — הכלי גוזר אוטומטית מ-appeal_subtype (`extension_request_*` → 'בל"מ') או מתבנית הטקסט. עדיף מפורש.
|
||||||
|
|
||||||
**הכלי שומר `source_kind='internal_committee'`.** DB constraint `case_law_internal_district_check` אוכף ש-`district NOT NULL` כשמדובר ב-internal_committee.
|
**הכלי שומר `source_kind='internal_committee'`.** DB constraint `case_law_internal_district_check` אוכף ש-`district NOT NULL` כשמדובר ב-internal_committee.
|
||||||
|
|
||||||
### אם chair_name או district חסר ב-PDF
|
### אם chair_name או district חסר ב-PDF
|
||||||
|
|||||||
@@ -823,6 +823,58 @@ CREATE TABLE IF NOT EXISTS legal_argument_propositions (
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# proceeding_type — מבחין בין הליך ערר עיקרי לבל"מ (בקשה להארכת מועד).
|
||||||
|
# חל גם על case_law (קורפוס) וגם על cases (תיקים חיים). שני הסוגים
|
||||||
|
# יכולים לחלוק אותו case_number, ולכן ה-uniqueness עוברת ל-(case_number,
|
||||||
|
# proceeding_type). בקורפוס: רק internal_committee מקבלים ערך מאוכלס;
|
||||||
|
# פסיקה חיצונית נשארת עם ''.
|
||||||
|
SCHEMA_V15_SQL = """
|
||||||
|
-- ------- case_law (קורפוס) -------
|
||||||
|
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS proceeding_type TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE case_law DROP CONSTRAINT IF EXISTS case_law_proceeding_type_check;
|
||||||
|
ALTER TABLE case_law ADD CONSTRAINT case_law_proceeding_type_check
|
||||||
|
CHECK (proceeding_type IN ('', 'ערר', 'בל"מ'));
|
||||||
|
|
||||||
|
-- Backfill לפי appeal_subtype הקיים
|
||||||
|
UPDATE case_law SET proceeding_type = 'בל"מ'
|
||||||
|
WHERE source_kind = 'internal_committee' AND proceeding_type = ''
|
||||||
|
AND appeal_subtype LIKE 'extension_request_%';
|
||||||
|
|
||||||
|
UPDATE case_law SET proceeding_type = 'ערר'
|
||||||
|
WHERE source_kind = 'internal_committee' AND proceeding_type = '';
|
||||||
|
|
||||||
|
ALTER TABLE case_law DROP CONSTRAINT IF EXISTS case_law_internal_proceeding_check;
|
||||||
|
ALTER TABLE case_law ADD CONSTRAINT case_law_internal_proceeding_check
|
||||||
|
CHECK (source_kind != 'internal_committee' OR proceeding_type IN ('ערר', 'בל"מ'));
|
||||||
|
|
||||||
|
-- החלפת UNIQUE(case_number) ב-partial unique לפי source_kind
|
||||||
|
ALTER TABLE case_law DROP CONSTRAINT IF EXISTS case_law_case_number_key;
|
||||||
|
DROP INDEX IF EXISTS case_law_case_number_key;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_case_law_internal_number_proc
|
||||||
|
ON case_law (case_number, proceeding_type)
|
||||||
|
WHERE source_kind = 'internal_committee';
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_case_law_external_number
|
||||||
|
ON case_law (case_number)
|
||||||
|
WHERE source_kind <> 'internal_committee';
|
||||||
|
|
||||||
|
-- ------- cases (תיקים חיים) -------
|
||||||
|
ALTER TABLE cases ADD COLUMN IF NOT EXISTS proceeding_type TEXT NOT NULL DEFAULT 'ערר';
|
||||||
|
|
||||||
|
ALTER TABLE cases DROP CONSTRAINT IF EXISTS cases_proceeding_type_check;
|
||||||
|
ALTER TABLE cases ADD CONSTRAINT cases_proceeding_type_check
|
||||||
|
CHECK (proceeding_type IN ('ערר', 'בל"מ'));
|
||||||
|
|
||||||
|
UPDATE cases SET proceeding_type = 'בל"מ'
|
||||||
|
WHERE proceeding_type = 'ערר' AND appeal_subtype LIKE 'extension_request_%';
|
||||||
|
|
||||||
|
ALTER TABLE cases DROP CONSTRAINT IF EXISTS cases_case_number_key;
|
||||||
|
DROP INDEX IF EXISTS cases_case_number_key;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_cases_number_proc
|
||||||
|
ON cases (case_number, proceeding_type);
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
|
async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
await conn.execute(SCHEMA_SQL)
|
await conn.execute(SCHEMA_SQL)
|
||||||
@@ -840,7 +892,8 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
|
|||||||
await conn.execute(SCHEMA_V12_SQL)
|
await conn.execute(SCHEMA_V12_SQL)
|
||||||
await conn.execute(SCHEMA_V13_SQL)
|
await conn.execute(SCHEMA_V13_SQL)
|
||||||
await conn.execute(SCHEMA_V14_SQL)
|
await conn.execute(SCHEMA_V14_SQL)
|
||||||
logger.info("Database schema initialized (v1-v14)")
|
await conn.execute(SCHEMA_V15_SQL)
|
||||||
|
logger.info("Database schema initialized (v1-v15)")
|
||||||
|
|
||||||
|
|
||||||
async def init_schema() -> None:
|
async def init_schema() -> None:
|
||||||
@@ -867,6 +920,7 @@ async def create_case(
|
|||||||
# from the case_number prefix before calling here.
|
# from the case_number prefix before calling here.
|
||||||
practice_area: str = "",
|
practice_area: str = "",
|
||||||
appeal_subtype: str = "",
|
appeal_subtype: str = "",
|
||||||
|
proceeding_type: str = "ערר",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
case_id = uuid4()
|
case_id = uuid4()
|
||||||
@@ -875,14 +929,14 @@ async def create_case(
|
|||||||
"""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)
|
practice_area, appeal_subtype, proceeding_type)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)""",
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)""",
|
||||||
case_id, case_number, title,
|
case_id, case_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,
|
practice_area, appeal_subtype, proceeding_type,
|
||||||
)
|
)
|
||||||
return await get_case(case_id)
|
return await get_case(case_id)
|
||||||
|
|
||||||
@@ -2024,18 +2078,22 @@ async def create_internal_committee_decision(
|
|||||||
summary: str = "",
|
summary: str = "",
|
||||||
is_binding: bool = True,
|
is_binding: bool = True,
|
||||||
document_id: UUID | None = None,
|
document_id: UUID | None = None,
|
||||||
|
proceeding_type: str = "ערר",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Upsert an appeals-committee decision as source_kind='internal_committee'.
|
"""Upsert an appeals-committee decision as source_kind='internal_committee'.
|
||||||
|
|
||||||
If a row with this case_number already exists as cited_only, promotes it.
|
Idempotency key: (case_number, proceeding_type) — the same number can
|
||||||
Idempotent: calling again updates metadata in-place.
|
exist as both 'ערר' and 'בל"מ' (an extension-of-time request can be
|
||||||
|
filed against an existing appeal with the same number).
|
||||||
"""
|
"""
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
tags_json = json.dumps(subject_tags or [], ensure_ascii=False)
|
tags_json = json.dumps(subject_tags or [], ensure_ascii=False)
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
existing = await conn.fetchrow(
|
existing = await conn.fetchrow(
|
||||||
"SELECT id FROM case_law WHERE case_number = $1",
|
"SELECT id FROM case_law "
|
||||||
case_number,
|
"WHERE case_number = $1 AND proceeding_type = $2 "
|
||||||
|
" AND source_kind = 'internal_committee'",
|
||||||
|
case_number, proceeding_type,
|
||||||
)
|
)
|
||||||
if existing:
|
if existing:
|
||||||
row = await conn.fetchrow(
|
row = await conn.fetchrow(
|
||||||
@@ -2072,19 +2130,20 @@ async def create_internal_committee_decision(
|
|||||||
subject_tags, summary, full_text,
|
subject_tags, summary, full_text,
|
||||||
source_kind, source_type, document_id,
|
source_kind, source_type, document_id,
|
||||||
extraction_status, halacha_extraction_status,
|
extraction_status, halacha_extraction_status,
|
||||||
practice_area, appeal_subtype, is_binding
|
practice_area, appeal_subtype, is_binding, proceeding_type
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6,
|
$1, $2, $3, $4, $5, $6,
|
||||||
$7, $8, $9,
|
$7, $8, $9,
|
||||||
'internal_committee', 'appeals_committee', $10,
|
'internal_committee', 'appeals_committee', $10,
|
||||||
'processing', 'pending',
|
'processing', 'pending',
|
||||||
$11, $12, $13
|
$11, $12, $13, $14
|
||||||
)
|
)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
""",
|
""",
|
||||||
case_number, case_name, court, decision_date, chair_name, district,
|
case_number, case_name, court, decision_date, chair_name, district,
|
||||||
tags_json, summary, full_text,
|
tags_json, summary, full_text,
|
||||||
document_id, practice_area, appeal_subtype, is_binding,
|
document_id, practice_area, appeal_subtype, is_binding,
|
||||||
|
proceeding_type,
|
||||||
)
|
)
|
||||||
return _row_to_case_law(row)
|
return _row_to_case_law(row)
|
||||||
|
|
||||||
@@ -2100,6 +2159,7 @@ async def update_case_law(case_law_id: UUID, **fields) -> dict | None:
|
|||||||
"case_number", "case_name", "court", "date", "practice_area", "appeal_subtype",
|
"case_number", "case_name", "court", "date", "practice_area", "appeal_subtype",
|
||||||
"subject_tags", "summary", "headnote", "key_quote", "source_url",
|
"subject_tags", "summary", "headnote", "key_quote", "source_url",
|
||||||
"source_type", "precedent_level", "is_binding", "district", "chair_name",
|
"source_type", "precedent_level", "is_binding", "district", "chair_name",
|
||||||
|
"proceeding_type",
|
||||||
}
|
}
|
||||||
updates = {k: v for k, v in fields.items() if k in allowed}
|
updates = {k: v for k, v in fields.items() if k in allowed}
|
||||||
if not updates:
|
if not updates:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from uuid import UUID, uuid4
|
|||||||
|
|
||||||
from legal_mcp import config
|
from legal_mcp import config
|
||||||
from legal_mcp.services import chunker, db, embeddings, extractor
|
from legal_mcp.services import chunker, db, embeddings, extractor
|
||||||
|
from legal_mcp.services.practice_area import derive_proceeding_type
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -86,11 +87,13 @@ async def ingest_internal_decision(
|
|||||||
text: str | None = None,
|
text: str | None = None,
|
||||||
document_id: UUID | None = None,
|
document_id: UUID | None = None,
|
||||||
queue_halachot: bool = True,
|
queue_halachot: bool = True,
|
||||||
|
proceeding_type: str = "",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Ingest an appeals-committee decision into the internal corpus.
|
"""Ingest an appeals-committee decision into the internal corpus.
|
||||||
|
|
||||||
Either file_path or text must be provided.
|
Either file_path or text must be provided.
|
||||||
If district is empty, it is inferred from court.
|
If district is empty, it is inferred from court.
|
||||||
|
If proceeding_type is empty, it is derived from appeal_subtype/case_name.
|
||||||
Returns: {"status": "completed", "case_law_id": "...", "chunks": N}
|
Returns: {"status": "completed", "case_law_id": "...", "chunks": N}
|
||||||
"""
|
"""
|
||||||
if not file_path and not text:
|
if not file_path and not text:
|
||||||
@@ -99,6 +102,9 @@ async def ingest_internal_decision(
|
|||||||
raise ValueError("case_number is required")
|
raise ValueError("case_number is required")
|
||||||
|
|
||||||
resolved_district = district.strip() or _district_from_court(court)
|
resolved_district = district.strip() or _district_from_court(court)
|
||||||
|
resolved_proc = proceeding_type.strip() or derive_proceeding_type(
|
||||||
|
appeal_subtype=appeal_subtype, subject=case_name,
|
||||||
|
)
|
||||||
|
|
||||||
if file_path:
|
if file_path:
|
||||||
src = Path(file_path)
|
src = Path(file_path)
|
||||||
@@ -133,6 +139,7 @@ async def ingest_internal_decision(
|
|||||||
summary=summary.strip(),
|
summary=summary.strip(),
|
||||||
is_binding=is_binding,
|
is_binding=is_binding,
|
||||||
document_id=document_id,
|
document_id=document_id,
|
||||||
|
proceeding_type=resolved_proc,
|
||||||
)
|
)
|
||||||
case_law_id = UUID(str(record["id"]))
|
case_law_id = UUID(str(record["id"]))
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,18 @@ def is_blam_subtype(appeal_subtype: str) -> bool:
|
|||||||
return appeal_subtype in BLAM_SUBTYPES
|
return appeal_subtype in BLAM_SUBTYPES
|
||||||
|
|
||||||
|
|
||||||
|
def derive_proceeding_type(*, appeal_subtype: str = "", subject: str = "") -> str:
|
||||||
|
"""Return 'בל"מ' / 'ערר' for appeals-committee decisions/cases.
|
||||||
|
|
||||||
|
Priority: explicit subtype prefix → subject regex → default 'ערר'.
|
||||||
|
"""
|
||||||
|
if appeal_subtype and appeal_subtype.startswith("extension_request_"):
|
||||||
|
return 'בל"מ'
|
||||||
|
if subject and is_blam_subject(subject):
|
||||||
|
return 'בל"מ'
|
||||||
|
return "ערר"
|
||||||
|
|
||||||
|
|
||||||
def derive_domain_practice_area(case_number: str) -> str:
|
def derive_domain_practice_area(case_number: str) -> str:
|
||||||
"""Map a case_number prefix to a domain practice_area (axis B).
|
"""Map a case_number prefix to a domain practice_area (axis B).
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ METADATA_EXTRACTION_PROMPT = """אתה מסייע משפטי בכיר. קרא א
|
|||||||
"decision_date_iso": "YYYY-MM-DD — תאריך מתן ההחלטה כפי שמופיע בטקסט (בכותרת או בחתימה הסופית). אם לא ניתן לזהות במדויק — מחרוזת ריקה.",
|
"decision_date_iso": "YYYY-MM-DD — תאריך מתן ההחלטה כפי שמופיע בטקסט (בכותרת או בחתימה הסופית). אם לא ניתן לזהות במדויק — מחרוזת ריקה.",
|
||||||
"precedent_level": "אחד מ-4: 'עליון' / 'מנהלי' / 'ועדת_ערר_ארצית' / 'ועדת_ערר_מחוזית'. בחר לפי הערכאה שמסומנת בכותרת הפסק. אם לא ברור — מחרוזת ריקה.",
|
"precedent_level": "אחד מ-4: 'עליון' / 'מנהלי' / 'ועדת_ערר_ארצית' / 'ועדת_ערר_מחוזית'. בחר לפי הערכאה שמסומנת בכותרת הפסק. אם לא ברור — מחרוזת ריקה.",
|
||||||
"source_type": "אחד מ-2: 'court_ruling' (פסק דין של בית משפט — עליון/מנהלי) / 'appeals_committee' (החלטה של ועדת ערר). אם לא ברור — מחרוזת ריקה.",
|
"source_type": "אחד מ-2: 'court_ruling' (פסק דין של בית משפט — עליון/מנהלי) / 'appeals_committee' (החלטה של ועדת ערר). אם לא ברור — מחרוזת ריקה.",
|
||||||
|
"proceeding_type": "אחד מ-2 (רק להחלטות ועדת ערר): 'ערר' (הליך ערר עיקרי על החלטת ועדה מקומית) / 'בל\\\"מ' (בקשה להארכת מועד להגשת ערר). זהה דרך כותרת המסמך: 'ערר (ועדות ערר ...) NNNN/YY' → 'ערר'; 'בל\\\"מ NNNN/YY' או נושא 'בקשה להארכת מועד להגשת ערר' → 'בל\\\"מ'. בפסיקת בית משפט (לא ועדת ערר) — מחרוזת ריקה.",
|
||||||
"court": "שם הערכאה כפי שהוא מופיע בכותרת (למשל 'בית המשפט העליון', 'בית המשפט המחוזי בירושלים בשבתו כבית משפט לעניינים מנהליים', 'ועדת הערר לתכנון ובניה פיצויים והיטלי השבחה — מחוז ירושלים'). מחרוזת ריקה אם לא ניתן לזהות.",
|
"court": "שם הערכאה כפי שהוא מופיע בכותרת (למשל 'בית המשפט העליון', 'בית המשפט המחוזי בירושלים בשבתו כבית משפט לעניינים מנהליים', 'ועדת הערר לתכנון ובניה פיצויים והיטלי השבחה — מחוז ירושלים'). מחרוזת ריקה אם לא ניתן לזהות.",
|
||||||
"case_number_clean": "מספר הערר/תיק כפי שמופיע בכותרת — רק הספרות והאלכסון, למשל '1062/24' או '8031/21'. ללא המילה 'ערר', ללא שם הצדדים, ללא סוגריים. אם יש כמה עררים מאוחדים — הרשום הראשון. מחרוזת ריקה אם לא ניתן לזהות."
|
"case_number_clean": "מספר הערר/תיק כפי שמופיע בכותרת — רק הספרות והאלכסון, למשל '1062/24' או '8031/21'. ללא המילה 'ערר', ללא שם הצדדים, ללא סוגריים. אם יש כמה עררים מאוחדים — הרשום הראשון. מחרוזת ריקה אם לא ניתן לזהות."
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,7 @@ METADATA_EXTRACTION_PROMPT = """אתה מסייע משפטי בכיר. קרא א
|
|||||||
8. **precedent_level** — קבע לפי הערכאה: בית המשפט העליון = "עליון"; בית משפט מחוזי בשבתו כבית משפט לעניינים מנהליים = "מנהלי"; ועדת ערר ארצית = "ועדת_ערר_ארצית"; ועדת ערר מחוזית (כמו ועדות תכנון ובניה ירושלים/מחוז המרכז וכד') = "ועדת_ערר_מחוזית". השתמש ב-underscore כפי שמופיע — לא ברווח.
|
8. **precedent_level** — קבע לפי הערכאה: בית המשפט העליון = "עליון"; בית משפט מחוזי בשבתו כבית משפט לעניינים מנהליים = "מנהלי"; ועדת ערר ארצית = "ועדת_ערר_ארצית"; ועדת ערר מחוזית (כמו ועדות תכנון ובניה ירושלים/מחוז המרכז וכד') = "ועדת_ערר_מחוזית". השתמש ב-underscore כפי שמופיע — לא ברווח.
|
||||||
9. **source_type** — שני ערכים בלבד: "court_ruling" כשהמסמך הוא פסק דין/החלטה של בית משפט (עליון/בג"ץ/מנהלי/מחוזי); "appeals_committee" כשהמסמך הוא החלטה של ועדת ערר (ארצית או מחוזית). זה משלים את `precedent_level` — שני השדות צריכים להיות תואמים.
|
9. **source_type** — שני ערכים בלבד: "court_ruling" כשהמסמך הוא פסק דין/החלטה של בית משפט (עליון/בג"ץ/מנהלי/מחוזי); "appeals_committee" כשהמסמך הוא החלטה של ועדת ערר (ארצית או מחוזית). זה משלים את `precedent_level` — שני השדות צריכים להיות תואמים.
|
||||||
10. **court** — מהכותרת הראשית של הפסק. ניסוח מלא (לא קיצור). מחרוזת ריקה אם לא ניתן לזהות.
|
10. **court** — מהכותרת הראשית של הפסק. ניסוח מלא (לא קיצור). מחרוזת ריקה אם לא ניתן לזהות.
|
||||||
|
11. **proceeding_type** — חובה לזהות עבור החלטות ועדת ערר; ריק עבור פסיקת בית משפט. הסימן הברור: בכותרת הראשונה של המסמך כתוב "ערר (ועדות ערר ...) NNNN/YY" → 'ערר'; "בל\"מ NNNN/YY" או הנושא "בקשה להארכת מועד להגשת ערר" → 'בל\"מ'. שני הסוגים יכולים לחלוק אותו מספר תיק — לכן חשוב להבחין מפורשות.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -160,6 +162,10 @@ async def extract_metadata(case_law_id: UUID | str) -> dict:
|
|||||||
st = result["source_type"].strip()
|
st = result["source_type"].strip()
|
||||||
if st in {"court_ruling", "appeals_committee"}:
|
if st in {"court_ruling", "appeals_committee"}:
|
||||||
out["source_type"] = st
|
out["source_type"] = st
|
||||||
|
if isinstance(result.get("proceeding_type"), str):
|
||||||
|
pt = result["proceeding_type"].strip()
|
||||||
|
if pt in {"ערר", 'בל"מ', ""}:
|
||||||
|
out["proceeding_type"] = pt
|
||||||
if isinstance(result.get("court"), str):
|
if isinstance(result.get("court"), str):
|
||||||
out["court"] = result["court"].strip()
|
out["court"] = result["court"].strip()
|
||||||
if isinstance(result.get("case_number_clean"), str):
|
if isinstance(result.get("case_number_clean"), str):
|
||||||
@@ -267,6 +273,13 @@ async def apply_to_record(
|
|||||||
if c:
|
if c:
|
||||||
fields_to_update["court"] = c
|
fields_to_update["court"] = c
|
||||||
|
|
||||||
|
# proceeding_type — only fill for internal_committee rows (the field is
|
||||||
|
# meaningless for court rulings, which we keep as '').
|
||||||
|
if not (record.get("proceeding_type") or "").strip():
|
||||||
|
pt = (suggested.get("proceeding_type") or "").strip()
|
||||||
|
if pt and (record.get("source_kind") == "internal_committee"):
|
||||||
|
fields_to_update["proceeding_type"] = pt
|
||||||
|
|
||||||
if overwrite_case_number:
|
if overwrite_case_number:
|
||||||
cn = (suggested.get("case_number_clean") or "").strip()
|
cn = (suggested.get("case_number_clean") or "").strip()
|
||||||
if cn:
|
if cn:
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ async def case_create(
|
|||||||
expected_outcome: str = "",
|
expected_outcome: str = "",
|
||||||
practice_area: str = "",
|
practice_area: str = "",
|
||||||
appeal_subtype: str = "",
|
appeal_subtype: str = "",
|
||||||
|
proceeding_type: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""יצירת תיק ערר חדש.
|
"""יצירת תיק ערר חדש.
|
||||||
|
|
||||||
@@ -150,6 +151,7 @@ async def case_create(
|
|||||||
אוטומטית ממספר התיק (1xxx→רישוי, 8xxx→השבחה, 9xxx→197)
|
אוטומטית ממספר התיק (1xxx→רישוי, 8xxx→השבחה, 9xxx→197)
|
||||||
appeal_subtype: סוג ערר (building_permit / betterment_levy / compensation_197).
|
appeal_subtype: סוג ערר (building_permit / betterment_levy / compensation_197).
|
||||||
ריק = יוסק אוטומטית ממספר התיק
|
ריק = יוסק אוטומטית ממספר התיק
|
||||||
|
proceeding_type: 'ערר' / 'בל"מ'. ריק = יוסק מ-appeal_subtype/subject.
|
||||||
"""
|
"""
|
||||||
from datetime import date as date_type
|
from datetime import date as date_type
|
||||||
|
|
||||||
@@ -173,6 +175,11 @@ async def case_create(
|
|||||||
appeal_subtype = derived_subtype
|
appeal_subtype = derived_subtype
|
||||||
pa.validate(practice_area, appeal_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 = await db.create_case(
|
||||||
case_number=case_number,
|
case_number=case_number,
|
||||||
title=title,
|
title=title,
|
||||||
@@ -187,6 +194,7 @@ async def case_create(
|
|||||||
expected_outcome=expected_outcome,
|
expected_outcome=expected_outcome,
|
||||||
practice_area=practice_area,
|
practice_area=practice_area,
|
||||||
appeal_subtype=appeal_subtype,
|
appeal_subtype=appeal_subtype,
|
||||||
|
proceeding_type=resolved_proc,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -290,6 +298,7 @@ async def case_update(
|
|||||||
respondents: list[str] | None = None,
|
respondents: list[str] | None = None,
|
||||||
property_address: str = "",
|
property_address: str = "",
|
||||||
permit_number: str = "",
|
permit_number: str = "",
|
||||||
|
proceeding_type: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""עדכון פרטי תיק.
|
"""עדכון פרטי תיק.
|
||||||
|
|
||||||
@@ -307,6 +316,7 @@ async def case_update(
|
|||||||
respondents: רשימת משיבים חדשה
|
respondents: רשימת משיבים חדשה
|
||||||
property_address: כתובת נכס חדשה
|
property_address: כתובת נכס חדשה
|
||||||
permit_number: מספר תכנית/בקשה חדש
|
permit_number: מספר תכנית/בקשה חדש
|
||||||
|
proceeding_type: 'ערר' / 'בל"מ' — ריק = ללא שינוי
|
||||||
"""
|
"""
|
||||||
from datetime import date as date_type
|
from datetime import date as date_type
|
||||||
|
|
||||||
@@ -359,6 +369,12 @@ async def case_update(
|
|||||||
fields["property_address"] = property_address
|
fields["property_address"] = property_address
|
||||||
if permit_number:
|
if permit_number:
|
||||||
fields["permit_number"] = 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)
|
updated = await db.update_case(UUID(case["id"]), **fields)
|
||||||
|
|
||||||
|
|||||||
@@ -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 Hebrew district names (matches _COURT_TO_DISTRICT in service)
|
||||||
VALID_DISTRICTS = {"ירושלים", "מרכז", "תל אביב", "תל-אביב", "צפון", "דרום", "חיפה", "ארצי"}
|
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:
|
def _ok(payload) -> str:
|
||||||
return json.dumps(payload, ensure_ascii=False, indent=2, default=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,
|
subject_tags: list[str] | None = None,
|
||||||
summary: str = "",
|
summary: str = "",
|
||||||
is_binding: bool = False,
|
is_binding: bool = False,
|
||||||
|
proceeding_type: str = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.
|
"""העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי.
|
||||||
|
|
||||||
@@ -62,6 +67,7 @@ async def internal_decision_upload(
|
|||||||
appeal_subtype: building_permit / וכו'.
|
appeal_subtype: building_permit / וכו'.
|
||||||
subject_tags: תגיות נושא.
|
subject_tags: תגיות נושא.
|
||||||
is_binding: בד"כ False (ועדת ערר לא מחייבת ועדה אחרת — שכנוע אופקי).
|
is_binding: בד"כ False (ועדת ערר לא מחייבת ועדה אחרת — שכנוע אופקי).
|
||||||
|
proceeding_type: 'ערר' או 'בל"מ'. אם ריק — נגזר מ-appeal_subtype/case_name.
|
||||||
|
|
||||||
Returns: JSON עם case_law_id, מספר chunks, halachot_pending.
|
Returns: JSON עם case_law_id, מספר chunks, halachot_pending.
|
||||||
"""
|
"""
|
||||||
@@ -83,6 +89,11 @@ async def internal_decision_upload(
|
|||||||
f"district לא תקין: {district!r}. ערכים תקפים: "
|
f"district לא תקין: {district!r}. ערכים תקפים: "
|
||||||
+ ", ".join(sorted(VALID_DISTRICTS))
|
+ ", ".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:
|
try:
|
||||||
result = await int_svc.ingest_internal_decision(
|
result = await int_svc.ingest_internal_decision(
|
||||||
@@ -98,6 +109,7 @@ async def internal_decision_upload(
|
|||||||
summary=summary,
|
summary=summary,
|
||||||
is_binding=is_binding,
|
is_binding=is_binding,
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
|
proceeding_type=proceeding_type,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _err(str(e))
|
return _err(str(e))
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
| `multimodal_backfill.py` | python | Backfill voyage-multimodal-3 page embeddings על מסמכי תיקים קיימים. idempotent (skips by default), forces `MULTIMODAL_ENABLED=true` ל-run, רץ מהקונטיינר. שלב C — ראה `docs/voyage-upgrades-plan.md` | ידני per-case (`python multimodal_backfill.py 8174-24 8137-24`) |
|
| `multimodal_backfill.py` | python | Backfill voyage-multimodal-3 page embeddings על מסמכי תיקים קיימים. idempotent (skips by default), forces `MULTIMODAL_ENABLED=true` ל-run, רץ מהקונטיינר. שלב C — ראה `docs/voyage-upgrades-plan.md` | ידני per-case (`python multimodal_backfill.py 8174-24 8137-24`) |
|
||||||
| `backfill_chunk_pages.py` | python | Backfill `page_number` ב-`document_chunks` קיימים. legacy chunker לא tracked עמודים → `page_number=NULL` חוסם boost של multimodal hybrid (text+image join על אותו עמוד). re-extracts כל PDF (re-OCR אם צריך, ~$0.0015/page), מחשב page_offsets, ומעדכן chunks. idempotent | ידני per-case (`python backfill_chunk_pages.py 8174-24 8137-24`) |
|
| `backfill_chunk_pages.py` | python | Backfill `page_number` ב-`document_chunks` קיימים. legacy chunker לא tracked עמודים → `page_number=NULL` חוסם boost של multimodal hybrid (text+image join על אותו עמוד). re-extracts כל PDF (re-OCR אם צריך, ~$0.0015/page), מחשב page_offsets, ומעדכן chunks. idempotent | ידני per-case (`python backfill_chunk_pages.py 8174-24 8137-24`) |
|
||||||
| `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/` בהזדמנות |
|
||||||
|
|
||||||
## תיקיית `.archive/` — סקריפטים שהושלמו
|
## תיקיית `.archive/` — סקריפטים שהושלמו
|
||||||
|
|
||||||
|
|||||||
60
scripts/upload_blam_decisions.py
Normal file
60
scripts/upload_blam_decisions.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
"""One-shot uploader for the 2 new בל"מ decisions Chaim staged in
|
||||||
|
data/precedents/incoming/. Bypasses MCP because the running MCP server
|
||||||
|
was started before SCHEMA_V15 + proceeding_type wiring landed.
|
||||||
|
|
||||||
|
Run from /home/chaim/legal-ai with the venv:
|
||||||
|
POSTGRES_URL=... .venv/bin/python scripts/upload_blam_decisions.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src"))
|
||||||
|
|
||||||
|
from legal_mcp.services import internal_decisions as svc
|
||||||
|
|
||||||
|
DECISIONS = [
|
||||||
|
{
|
||||||
|
"file_path": "/home/chaim/legal-ai/data/precedents/incoming/ARAR-24-8126.pdf",
|
||||||
|
"case_number": "8126/24",
|
||||||
|
"chair_name": "דפנה תמיר",
|
||||||
|
"district": "ירושלים",
|
||||||
|
"case_name": "הוועדה המקומית ירושלים נ' סופר נוח",
|
||||||
|
"court": "ועדת הערר לתכנון ובנייה — מחוז ירושלים",
|
||||||
|
"decision_date": "2024-07-07",
|
||||||
|
"practice_area": "betterment_levy",
|
||||||
|
"appeal_subtype": "extension_request_betterment_levy",
|
||||||
|
"proceeding_type": 'בל"מ',
|
||||||
|
"subject_tags": ["בקשה_להארכת_מועד", "היטל_השבחה"],
|
||||||
|
"summary": "",
|
||||||
|
"is_binding": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_path": "/home/chaim/legal-ai/data/precedents/incoming/ARAR-23-8047-3.docx",
|
||||||
|
"case_number": "8047/23",
|
||||||
|
"chair_name": "דפנה תמיר",
|
||||||
|
"district": "ירושלים",
|
||||||
|
"case_name": 'עזבון אליהו הרנון ז"ל נ\' הוועדה המקומית ירושלים',
|
||||||
|
"court": "ועדת הערר לתכנון ובנייה — מחוז ירושלים",
|
||||||
|
"decision_date": "2025-09-29",
|
||||||
|
"practice_area": "betterment_levy",
|
||||||
|
"appeal_subtype": "extension_request_betterment_levy",
|
||||||
|
"proceeding_type": 'בל"מ',
|
||||||
|
"subject_tags": ["בקשה_להארכת_מועד", "היטל_השבחה"],
|
||||||
|
"summary": "",
|
||||||
|
"is_binding": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
for d in DECISIONS:
|
||||||
|
print(f"→ uploading {d['case_number']} ({d['proceeding_type']})")
|
||||||
|
result = await svc.ingest_internal_decision(**d)
|
||||||
|
print(f" ✓ case_law_id={result.get('case_law_id')} chunks={result.get('chunks')}")
|
||||||
|
print("done.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -17,7 +17,10 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { PartiesField } from "@/components/wizard/parties-field";
|
import { PartiesField } from "@/components/wizard/parties-field";
|
||||||
import { useUpdateCase } from "@/lib/api/cases";
|
import { useUpdateCase } from "@/lib/api/cases";
|
||||||
import { caseUpdateSchema, expectedOutcomes, type CaseUpdateInput } from "@/lib/schemas/case";
|
import {
|
||||||
|
caseUpdateSchema, expectedOutcomes, proceedingTypes,
|
||||||
|
type CaseUpdateInput,
|
||||||
|
} from "@/lib/schemas/case";
|
||||||
import type { CaseDetail } from "@/lib/api/cases";
|
import type { CaseDetail } from "@/lib/api/cases";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -47,6 +50,7 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
|
|||||||
respondents: data.respondents ?? [],
|
respondents: data.respondents ?? [],
|
||||||
property_address: data.property_address ?? "",
|
property_address: data.property_address ?? "",
|
||||||
permit_number: data.permit_number ?? "",
|
permit_number: data.permit_number ?? "",
|
||||||
|
proceeding_type: data.proceeding_type ?? "ערר",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -63,6 +67,7 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
|
|||||||
respondents: data.respondents ?? [],
|
respondents: data.respondents ?? [],
|
||||||
property_address: data.property_address ?? "",
|
property_address: data.property_address ?? "",
|
||||||
permit_number: data.permit_number ?? "",
|
permit_number: data.permit_number ?? "",
|
||||||
|
proceeding_type: data.proceeding_type ?? "ערר",
|
||||||
});
|
});
|
||||||
}, [open, data, form]);
|
}, [open, data, form]);
|
||||||
|
|
||||||
@@ -104,6 +109,37 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
|
|||||||
<FieldError message={form.formState.errors.subject?.message} />
|
<FieldError message={form.formState.errors.subject?.message} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-navy">סוג תיק</Label>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="proceeding_type"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
value={field.value ?? "ערר"}
|
||||||
|
onValueChange={(v) =>
|
||||||
|
field.onChange(v as CaseUpdateInput["proceeding_type"])
|
||||||
|
}
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{proceedingTypes.map((p) => (
|
||||||
|
<SelectItem key={p.value} value={p.value}>
|
||||||
|
{p.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className="text-[0.7rem] text-ink-muted mt-1">
|
||||||
|
ערר = הליך עיקרי; בל"מ = בקשה להארכת מועד להגשת ערר
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="h-px bg-rule" />
|
<div className="h-px bg-rule" />
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export function CaseHeader({ data }: { data?: CaseDetail }) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-3 flex-wrap">
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
<span className="font-display text-[2rem] font-black text-navy leading-none tabular-nums">
|
<span className="font-display text-[2rem] font-black text-navy leading-none tabular-nums">
|
||||||
ערר {data?.case_number ?? "—"}
|
{data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"}
|
||||||
</span>
|
</span>
|
||||||
{data?.status && <StatusBadge status={data.status} />}
|
{data?.status && <StatusBadge status={data.status} />}
|
||||||
{data?.archived_at && (
|
{data?.archived_at && (
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { PartiesField } from "@/components/wizard/parties-field";
|
import { PartiesField } from "@/components/wizard/parties-field";
|
||||||
import { useCreateCase } from "@/lib/api/cases";
|
import { useCreateCase } from "@/lib/api/cases";
|
||||||
import {
|
import {
|
||||||
caseCreateSchema, expectedOutcomes,
|
caseCreateSchema, expectedOutcomes, proceedingTypes,
|
||||||
type CaseCreateInput,
|
type CaseCreateInput,
|
||||||
} from "@/lib/schemas/case";
|
} from "@/lib/schemas/case";
|
||||||
import {
|
import {
|
||||||
@@ -35,7 +35,7 @@ type StepKey = (typeof STEPS)[number]["key"];
|
|||||||
/* Fields validated at each step — lets the user fix just what's on screen
|
/* Fields validated at each step — lets the user fix just what's on screen
|
||||||
* before moving forward, instead of surfacing all errors from page 1. */
|
* before moving forward, instead of surfacing all errors from page 1. */
|
||||||
const STEP_FIELDS: Record<StepKey, (keyof CaseCreateInput)[]> = {
|
const STEP_FIELDS: Record<StepKey, (keyof CaseCreateInput)[]> = {
|
||||||
basics: ["case_number", "title", "practice_area", "appeal_subtype"],
|
basics: ["case_number", "title", "proceeding_type", "practice_area", "appeal_subtype"],
|
||||||
parties: ["appellants", "respondents"],
|
parties: ["appellants", "respondents"],
|
||||||
details: ["subject", "hearing_date", "expected_outcome", "notes", "property_address", "permit_number"],
|
details: ["subject", "hearing_date", "expected_outcome", "notes", "property_address", "permit_number"],
|
||||||
};
|
};
|
||||||
@@ -66,6 +66,7 @@ export function CaseWizard() {
|
|||||||
expected_outcome: "",
|
expected_outcome: "",
|
||||||
practice_area: "appeals_committee",
|
practice_area: "appeals_committee",
|
||||||
appeal_subtype: "unknown",
|
appeal_subtype: "unknown",
|
||||||
|
proceeding_type: "ערר",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,11 +75,17 @@ export function CaseWizard() {
|
|||||||
* stop the moment they manually pick a value from the dropdown. Mirrors
|
* stop the moment they manually pick a value from the dropdown. Mirrors
|
||||||
* the wireSubtypeAutofill() behaviour of the vanilla UI
|
* the wireSubtypeAutofill() behaviour of the vanilla UI
|
||||||
* (legal-ai/web/static/index.html around line 2770).
|
* (legal-ai/web/static/index.html around line 2770).
|
||||||
|
*
|
||||||
|
* proceeding_type follows the same pattern: if the user hasn't picked
|
||||||
|
* a value yet, we derive 'בל"מ' whenever the subtype lands on an
|
||||||
|
* extension_request_* value.
|
||||||
*/
|
*/
|
||||||
const userTouchedSubtype = useRef(false);
|
const userTouchedSubtype = useRef(false);
|
||||||
|
const userTouchedProceeding = useRef(false);
|
||||||
const caseNumber = form.watch("case_number");
|
const caseNumber = form.watch("case_number");
|
||||||
const practiceArea = form.watch("practice_area");
|
const practiceArea = form.watch("practice_area");
|
||||||
const subject = form.watch("subject");
|
const subject = form.watch("subject");
|
||||||
|
const appealSubtype = form.watch("appeal_subtype");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (userTouchedSubtype.current) return;
|
if (userTouchedSubtype.current) return;
|
||||||
/* derive_subtype_with_blam picks extension_request_* when subject
|
/* derive_subtype_with_blam picks extension_request_* when subject
|
||||||
@@ -89,6 +96,16 @@ export function CaseWizard() {
|
|||||||
}
|
}
|
||||||
}, [caseNumber, practiceArea, subject, form]);
|
}, [caseNumber, practiceArea, subject, form]);
|
||||||
|
|
||||||
|
/* proceeding_type follows appeal_subtype when the user hasn't picked
|
||||||
|
* one explicitly — extension_request_* always implies 'בל"מ'. */
|
||||||
|
useEffect(() => {
|
||||||
|
if (userTouchedProceeding.current) return;
|
||||||
|
const proc = appealSubtype.startsWith("extension_request_") ? 'בל"מ' : "ערר";
|
||||||
|
if (proc !== form.getValues("proceeding_type")) {
|
||||||
|
form.setValue("proceeding_type", proc, { shouldValidate: false });
|
||||||
|
}
|
||||||
|
}, [appealSubtype, form]);
|
||||||
|
|
||||||
const stepIndex = STEPS.findIndex((s) => s.key === step);
|
const stepIndex = STEPS.findIndex((s) => s.key === step);
|
||||||
const isLast = stepIndex === STEPS.length - 1;
|
const isLast = stepIndex === STEPS.length - 1;
|
||||||
|
|
||||||
@@ -162,6 +179,39 @@ export function CaseWizard() {
|
|||||||
<Input id="title" {...form.register("title")} className="mt-1" />
|
<Input id="title" {...form.register("title")} className="mt-1" />
|
||||||
<FieldError message={form.formState.errors.title?.message} />
|
<FieldError message={form.formState.errors.title?.message} />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-navy">
|
||||||
|
סוג תיק <span className="text-danger">*</span>
|
||||||
|
</Label>
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="proceeding_type"
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
userTouchedProceeding.current = true;
|
||||||
|
field.onChange(v as CaseCreateInput["proceeding_type"]);
|
||||||
|
}}
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{proceedingTypes.map((p) => (
|
||||||
|
<SelectItem key={p.value} value={p.value}>
|
||||||
|
{p.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<p className="text-[0.7rem] text-ink-muted mt-1">
|
||||||
|
ערר = הליך עיקרי; בל"מ = בקשה להארכת מועד להגשת ערר
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-navy">תחום משפטי</Label>
|
<Label className="text-navy">תחום משפטי</Label>
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export type Case = {
|
|||||||
respondents?: string[] | null;
|
respondents?: string[] | null;
|
||||||
property_address?: string | null;
|
property_address?: string | null;
|
||||||
permit_number?: string | null;
|
permit_number?: string | null;
|
||||||
|
/* 'ערר' = regular appeal, 'בל"מ' = extension-of-time request */
|
||||||
|
proceeding_type?: "ערר" | 'בל"מ';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CaseDocument = {
|
export type CaseDocument = {
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ export const expectedOutcomes = [
|
|||||||
{ value: "betterment_levy", label: "היטל השבחה" },
|
{ value: "betterment_levy", label: "היטל השבחה" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
/* proceeding_type — distinguishes a regular appeal (ערר) from an
|
||||||
|
* extension-of-time request (בל"מ). The same case_number can exist as
|
||||||
|
* both, so this is a separate axis from appeal_subtype/practice_area. */
|
||||||
|
export const proceedingTypes = [
|
||||||
|
{ value: "ערר", label: "ערר" },
|
||||||
|
{ value: 'בל"מ', label: 'בל"מ' },
|
||||||
|
] as const;
|
||||||
|
export type ProceedingType = (typeof proceedingTypes)[number]["value"];
|
||||||
|
|
||||||
export const caseCreateSchema = z.object({
|
export const caseCreateSchema = z.object({
|
||||||
case_number: z
|
case_number: z
|
||||||
.string()
|
.string()
|
||||||
@@ -75,6 +84,7 @@ export const caseCreateSchema = z.object({
|
|||||||
"extension_request_compensation",
|
"extension_request_compensation",
|
||||||
"unknown",
|
"unknown",
|
||||||
] as const satisfies readonly AppealSubtype[]),
|
] as const satisfies readonly AppealSubtype[]),
|
||||||
|
proceeding_type: z.enum(["ערר", 'בל"מ'] as const),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CaseCreateInput = z.infer<typeof caseCreateSchema>;
|
export type CaseCreateInput = z.infer<typeof caseCreateSchema>;
|
||||||
@@ -100,6 +110,7 @@ export const caseUpdateSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
property_address: z.string().trim().max(200).optional(),
|
property_address: z.string().trim().max(200).optional(),
|
||||||
permit_number: z.string().trim().max(100).optional(),
|
permit_number: z.string().trim().max(100).optional(),
|
||||||
|
proceeding_type: z.enum(["ערר", 'בל"מ'] as const).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CaseUpdateInput = z.infer<typeof caseUpdateSchema>;
|
export type CaseUpdateInput = z.infer<typeof caseUpdateSchema>;
|
||||||
|
|||||||
@@ -1234,6 +1234,9 @@ class CaseCreateRequest(BaseModel):
|
|||||||
# send a domain value explicitly.
|
# send a domain value explicitly.
|
||||||
practice_area: str = ""
|
practice_area: str = ""
|
||||||
appeal_subtype: str = ""
|
appeal_subtype: str = ""
|
||||||
|
# proceeding_type: 'ערר' / 'בל"מ'. Empty → auto-derived from
|
||||||
|
# appeal_subtype / subject downstream.
|
||||||
|
proceeding_type: str = ""
|
||||||
|
|
||||||
|
|
||||||
class CaseUpdateRequest(BaseModel):
|
class CaseUpdateRequest(BaseModel):
|
||||||
@@ -1249,6 +1252,7 @@ class CaseUpdateRequest(BaseModel):
|
|||||||
respondents: list[str] | None = None
|
respondents: list[str] | None = None
|
||||||
property_address: str = ""
|
property_address: str = ""
|
||||||
permit_number: str = ""
|
permit_number: str = ""
|
||||||
|
proceeding_type: str = ""
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/cases/create")
|
@app.post("/api/cases/create")
|
||||||
@@ -1268,6 +1272,7 @@ async def api_case_create(req: CaseCreateRequest):
|
|||||||
expected_outcome=req.expected_outcome,
|
expected_outcome=req.expected_outcome,
|
||||||
practice_area=req.practice_area,
|
practice_area=req.practice_area,
|
||||||
appeal_subtype=req.appeal_subtype,
|
appeal_subtype=req.appeal_subtype,
|
||||||
|
proceeding_type=req.proceeding_type,
|
||||||
)
|
)
|
||||||
parsed = json.loads(result)
|
parsed = json.loads(result)
|
||||||
|
|
||||||
@@ -1399,6 +1404,7 @@ async def api_case_update(case_number: str, req: CaseUpdateRequest, background_t
|
|||||||
respondents=req.respondents,
|
respondents=req.respondents,
|
||||||
property_address=req.property_address,
|
property_address=req.property_address,
|
||||||
permit_number=req.permit_number,
|
permit_number=req.permit_number,
|
||||||
|
proceeding_type=req.proceeding_type,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(422, str(exc))
|
raise HTTPException(422, str(exc))
|
||||||
|
|||||||
Reference in New Issue
Block a user