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

@@ -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 with pool.acquire() as conn:
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_V13_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:
@@ -867,6 +920,7 @@ async def create_case(
# from the case_number prefix before calling here.
practice_area: str = "",
appeal_subtype: str = "",
proceeding_type: str = "ערר",
) -> dict:
pool = await get_pool()
case_id = uuid4()
@@ -875,14 +929,14 @@ async def create_case(
"""INSERT INTO cases (id, case_number, title, appellants, respondents,
subject, property_address, permit_number, committee_type,
hearing_date, notes, expected_outcome,
practice_area, appeal_subtype)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)""",
practice_area, appeal_subtype, proceeding_type)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)""",
case_id, case_number, title,
json.dumps(appellants or []),
json.dumps(respondents or []),
subject, property_address, permit_number, committee_type,
hearing_date, notes, expected_outcome,
practice_area, appeal_subtype,
practice_area, appeal_subtype, proceeding_type,
)
return await get_case(case_id)
@@ -2024,18 +2078,22 @@ async def create_internal_committee_decision(
summary: str = "",
is_binding: bool = True,
document_id: UUID | None = None,
proceeding_type: str = "ערר",
) -> dict:
"""Upsert an appeals-committee decision as source_kind='internal_committee'.
If a row with this case_number already exists as cited_only, promotes it.
Idempotent: calling again updates metadata in-place.
Idempotency key: (case_number, proceeding_type) — the same number can
exist as both 'ערר' and 'בל"מ' (an extension-of-time request can be
filed against an existing appeal with the same number).
"""
pool = await get_pool()
tags_json = json.dumps(subject_tags or [], ensure_ascii=False)
async with pool.acquire() as conn:
existing = await conn.fetchrow(
"SELECT id FROM case_law WHERE case_number = $1",
case_number,
"SELECT id FROM case_law "
"WHERE case_number = $1 AND proceeding_type = $2 "
" AND source_kind = 'internal_committee'",
case_number, proceeding_type,
)
if existing:
row = await conn.fetchrow(
@@ -2072,19 +2130,20 @@ async def create_internal_committee_decision(
subject_tags, summary, full_text,
source_kind, source_type, document_id,
extraction_status, halacha_extraction_status,
practice_area, appeal_subtype, is_binding
practice_area, appeal_subtype, is_binding, proceeding_type
) VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9,
'internal_committee', 'appeals_committee', $10,
'processing', 'pending',
$11, $12, $13
$11, $12, $13, $14
)
RETURNING *
""",
case_number, case_name, court, decision_date, chair_name, district,
tags_json, summary, full_text,
document_id, practice_area, appeal_subtype, is_binding,
proceeding_type,
)
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",
"subject_tags", "summary", "headnote", "key_quote", "source_url",
"source_type", "precedent_level", "is_binding", "district", "chair_name",
"proceeding_type",
}
updates = {k: v for k, v in fields.items() if k in allowed}
if not updates: