feat(precedents): formal citation per Israeli citation rules + copy/edit UI
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m25s

Until now, "case_number" was the only stored identifier for a precedent.
But a *citation per the Israeli unified citation rules* is a different
beast — it has bold parties, an unbold prefix (court abbrev + panel/
district parenthetical + case number), and an unbold trailing reporter
(נבו / פ"ד...).  Without storing it as a first-class field we couldn't
hand the chair a one-click "copy as citation" experience for pasting
into decisions.

Changes:
- Schema V19: case_law.citation_formatted TEXT (Markdown — parties
  wrapped in **…** so the copy helper can render <strong> for Word/Docs
  paste and keep plain-text fallback meaningful).
- Metadata extractor: composes citation_formatted from the document
  text per the unified citation rules, with worked examples for ע"א /
  עת"מ / ערר / בל"מ in the prompt. Refuses to store half-formed strings.
- PATCH /api/precedent-library/{id} accepts citation_formatted so the
  chair can correct LLM mistakes.
- /precedents/[id]: dedicated "מראה מקום" block with bold rendering,
  a copy-to-clipboard button (text/html + text/plain so Word keeps
  the bolds), and an inline edit textarea.
- /precedents list rows: link displays the formatted citation when
  available, with a small inline copy button — falls back to the bare
  case_number for older rows.

Backfill of existing rows happens by re-stamping the extraction queue
once V19 has rolled out and the new field is reachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 07:14:34 +00:00
parent a02a4e3a64
commit cbc7a1e336
7 changed files with 369 additions and 13 deletions

View File

@@ -1007,6 +1007,20 @@ CREATE INDEX IF NOT EXISTS idx_relevance_case_law
"""
# ── V19: case_law.citation_formatted ───────────────────────────────
# Full formal citation per the Israeli unified citation rules ("כללי
# הציטוט האחיד"). Stored as Markdown: parties wrapped in **…** so the
# copy-to-clipboard helper can render bold for Word/Docs while keeping
# the plain-text form readable.
#
# Example:
# ערר (ועדות ערר - תכנון ובנייה ת"א-יפו) 81002-01-21 **אברהם אגסי
# נ' הועדה המקומית לתכנון ובנייה תל אביב** (נבו 25.9.2025)
SCHEMA_V19_SQL = """
ALTER TABLE case_law ADD COLUMN IF NOT EXISTS citation_formatted TEXT DEFAULT '';
"""
async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
async with pool.acquire() as conn:
await conn.execute(SCHEMA_SQL)
@@ -1028,7 +1042,8 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
await conn.execute(SCHEMA_V16_SQL)
await conn.execute(SCHEMA_V17_SQL)
await conn.execute(SCHEMA_V18_SQL)
logger.info("Database schema initialized (v1-v18)")
await conn.execute(SCHEMA_V19_SQL)
logger.info("Database schema initialized (v1-v19)")
async def init_schema() -> None:
@@ -2288,13 +2303,13 @@ async def update_case_law(case_law_id: UUID, **fields) -> dict | None:
Allowed fields: case_name, court, date, practice_area, appeal_subtype,
subject_tags, summary, headnote, key_quote, source_url, source_type,
precedent_level, is_binding.
precedent_level, is_binding, citation_formatted.
"""
allowed = {
"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",
"proceeding_type", "citation_formatted",
}
updates = {k: v for k, v in fields.items() if k in allowed}
if not updates:
@@ -2405,7 +2420,7 @@ async def list_external_case_law(
SELECT id, case_number, case_name, court, date, practice_area,
appeal_subtype, source_type, precedent_level, is_binding,
summary, headnote, subject_tags, source_kind,
chair_name, district,
chair_name, district, citation_formatted,
extraction_status, halacha_extraction_status,
metadata_extraction_requested_at,
halacha_extraction_requested_at,