Files
legal-ai/mcp-server/src/legal_mcp/tools/missing_precedents.py
Chaim f3cc9ca9d4
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
feat: Stage A finalizers + #35/#36/#37 — critical-gap closure
Four parallel sub-agents closed the remaining critical gaps from the
26/05 Stage A/B sprint. Each block independently tested; aggregated here.

## #30/#31 finalizers (sub-agent A)
* Auto-derive practice_area in case_create from case_number prefix
  (1xxx→rishuy_uvniya, 8xxx→betterment_levy, 9xxx→compensation_197);
  default for CaseCreateRequest is now "" (the DB constraint catches
  any stray "appeals_committee").
* practice_area.py: derive_subtype now handles axis-B domain values
  (rishuy_uvniya/betterment_levy/compensation_197) without parsing the
  case number; new helper derive_domain_practice_area().
* Halacha re-extraction verified unnecessary — all 6 reclassified
  records already had is_binding=false and approved halachot.
* Regression tests: 6 cases in tests/test_corpus_constraints.py
  covering practice_area enum, internal-committee chair/district,
  external-upload arar prefix, MCP guard.
* UI: district input → Select dropdown (7 districts) in
  precedent-edit-sheet.tsx, preserving legacy free-text values.

## #37 בל"מ subtypes (sub-agent B)
* 3 new appeal_subtypes: extension_request_{building_permit,
  betterment_levy,compensation}. APPEALS_COMMITTEE_SUBTYPES extended,
  SUBTYPES_BY_AREA mappings added.
* New helpers: is_blam_subject(), is_blam_subtype(),
  derive_subtype_with_blam(case_number, subject, practice_area).
  case_create now uses it to auto-detect "בקשה להארכת מועד" subjects.
* 3 methodology templates under docs/methodology/extension-request-*.md.
* paperclip_client.py mapping updated for the 3 new subtypes
  (extension_request_building_permit→CMP, the other two→CMPA).
* Frontend: bilingual "בל"מ" badge + filter dropdown on cases list +
  detail header; appeal-type-bars collapseBlam() merges בל"מ into its
  parent domain for aggregate bars.
* Wizard auto-detects בל"מ from subject during case creation.
* 3 Berlinger cases (1017/1018/1019-03-26) migrated to
  appeal_subtype=extension_request_building_permit via psql.

## #35 missing_precedents feature (sub-agent C)
* Schema V13: missing_precedents table (citation, case_id, party,
  legal_topic, status, linked_case_law_id, claim_quote, ...) +
  FK constraints + 3 indexes. Applied via psql + idempotent migration.
* 6 db.py service functions, 3 MCP tools, 6 FastAPI endpoints
  (POST/GET/PATCH/DELETE/upload — upload routes by citation prefix
  to ingest_internal_decision or ingest_precedent).
* Next.js page /missing-precedents with 5 status tabs + filters +
  sidebar badge counter + detail drawer with metadata edit + smart
  upload form that switches fields per committee/court.
* Bootstrap: 7 rows imported from the JSON file
  (3 citations × cases, all status=closed with linked_case_law_id).
* legal-researcher.md: new §2ב.5 with missing_precedent_create
  usage + dedup semantics + tool grant.

## #36 legal_arguments aggregation (sub-agent D)
* Schema V14: legal_arguments + legal_argument_propositions M:M.
  Applied via psql.
* New service argument_aggregator.py with two functions —
  aggregate_claims_to_arguments() (Claude CLI / claude_session) and
  get_legal_arguments(). Graceful llm_unavailable handling when CLI
  is missing (containers).
* 2 MCP tools + 2 API endpoints (POST .../aggregate-arguments as
  BackgroundTask, GET .../legal-arguments).
* Frontend: shadcn Accordion + new legal-arguments-panel.tsx with
  hierarchical (party → priority badge → arguments) display, "טיעונים"
  tab on the case page, "חשב/חשב מחדש" buttons.
* scripts/backfill_legal_arguments.py + SCRIPTS.md entry — dry-run
  found 8 candidate cases including 1017/1018/1019.

## Open follow-ups (intentionally deferred)
* npm run api:types in web-ui (CLAUDE.md flow) — recommended before
  the next UI commit; not required for backend deployment.
* Run backfill_legal_arguments.py --apply once the container picks up
  the new aggregator service.
* webhook on missing-precedents upload-close to Paperclip (optional).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 08:34:40 +00:00

211 lines
7.2 KiB
Python

"""MCP tools for the missing-precedents log.
When a researcher (or chair) finds a citation in a party brief that
isn't yet in the precedent_library, they record it here so:
1. The gap is visible in the UI (the chair can see all open citations
that need to be uploaded).
2. The writer agent doesn't try to use a precedent that isn't in the
corpus — it knows the gap is being tracked.
3. The chair has a clean closing workflow: upload the actual decision
via the precedent library / internal-decisions, then link it here.
Three tools:
- ``missing_precedent_create`` — log a new gap (researcher / chair).
- ``missing_precedent_list`` — list open gaps (optionally filtered).
- ``missing_precedent_close`` — close a gap (chair workflow).
"""
from __future__ import annotations
import json
from uuid import UUID
from legal_mcp.services import db
def _ok(payload) -> str:
return json.dumps(payload, ensure_ascii=False, indent=2, default=str)
def _err(msg: str) -> str:
return json.dumps({"error": msg}, ensure_ascii=False)
async def _resolve_case_id(case_number: str) -> UUID | None:
"""Translate a human case_number (e.g. '1017-03-26') to a UUID."""
if not case_number or not case_number.strip():
return None
row = await db.get_case_by_number(case_number.strip())
if not row:
return None
return UUID(row["id"])
async def missing_precedent_create(
citation: str,
case_number: str = "",
cited_in_document_id: str = "",
cited_by_party: str = "unknown",
cited_by_party_name: str = "",
legal_topic: str = "",
legal_issue: str = "",
claim_quote: str = "",
case_name: str = "",
notes: str = "",
) -> str:
"""תיעוד פסיקה שצוטטה אך אינה בקורפוס. הסוכן יוצר רשומה כשהוא מזהה ציטוט
שלא ניתן לאמת מול הקורפוס; היו"ר יסגור אותה לאחר העלאת המסמך.
Args:
citation: מראה המקום המלא (חובה).
case_number: מספר תיק הערר שבו צוטטה הפסיקה (לדוגמה '1017-03-26').
cited_in_document_id: UUID של המסמך שבו הציטוט מופיע (אופציונלי).
cited_by_party: appellant / respondent / committee / permit_applicant / unknown.
cited_by_party_name: שם הצד (כדי שיהיה ברור מי ציטט).
legal_topic: נושא משפטי קצר (לדוגמה "זכות עמידה").
legal_issue: שאלה משפטית מפורטת.
claim_quote: הציטוט בכתב הטענות.
case_name: שם קצר של פסק הדין החסר.
notes: הערות חופשיות.
Returns: JSON של הרשומה שנוצרה (כולל id) או error.
"""
if not citation.strip():
return _err("citation חובה")
case_id = None
if case_number:
case_id = await _resolve_case_id(case_number)
if case_id is None:
return _err(f"תיק לא נמצא: {case_number}")
doc_uuid: UUID | None = None
if cited_in_document_id.strip():
try:
doc_uuid = UUID(cited_in_document_id.strip())
except ValueError:
return _err("cited_in_document_id לא תקין")
party = cited_by_party.strip() or "unknown"
if party not in db.ALLOWED_MP_PARTIES:
return _err(
f"cited_by_party לא תקין. ערכים תקפים: "
f"{', '.join(sorted(db.ALLOWED_MP_PARTIES))}"
)
# Deduplication: if a row already exists for the same citation in
# the same case, return that one rather than creating a duplicate.
existing = await db.find_missing_precedent_by_citation(
citation=citation.strip(),
case_id=case_id,
)
if existing:
return _ok({**existing, "_duplicate": True})
try:
row = await db.create_missing_precedent(
citation=citation.strip(),
case_name=case_name.strip() or None,
cited_in_case_id=case_id,
cited_in_document_id=doc_uuid,
cited_by_party=party,
cited_by_party_name=cited_by_party_name.strip() or None,
legal_topic=legal_topic.strip() or None,
legal_issue=legal_issue.strip() or None,
claim_quote=claim_quote.strip() or None,
notes=notes.strip() or None,
)
except Exception as e:
return _err(str(e))
return _ok(row)
async def missing_precedent_list(
case_number: str = "",
status: str = "open",
legal_topic: str = "",
limit: int = 50,
) -> str:
"""רשימת פסיקות חסרות. ברירת מחדל = פתוחות בלבד.
Args:
case_number: סינון לפי תיק הערר שבו צוטטו.
status: open / uploaded / closed / irrelevant (ריק = הכל).
legal_topic: סינון לפי נושא משפטי (substring).
limit: מספר תוצאות מקסימלי.
Returns: JSON עם רשימת רשומות + linked_case_law_number אם נסגרו.
"""
case_id = None
if case_number:
case_id = await _resolve_case_id(case_number)
if case_id is None:
return _err(f"תיק לא נמצא: {case_number}")
s = status.strip() or None
if s and s not in db.ALLOWED_MP_STATUS:
return _err(
f"status לא תקין. ערכים תקפים: "
f"{', '.join(sorted(db.ALLOWED_MP_STATUS))}"
)
try:
rows = await db.list_missing_precedents(
status=s,
case_id=case_id,
legal_topic=legal_topic.strip() or None,
limit=max(1, min(int(limit), 500)),
)
except Exception as e:
return _err(str(e))
return _ok({"items": rows, "count": len(rows)})
async def missing_precedent_close(
id: str,
linked_case_law_id: str = "",
notes: str = "",
status: str = "closed",
) -> str:
"""סגירת רשומת פסיקה חסרה. ברירת מחדל = 'closed' + קישור ל-case_law.
Args:
id: UUID של הרשומה.
linked_case_law_id: UUID של הפסיקה שהועלתה ב-precedent_library / internal_decisions.
notes: הערות סגירה (לדוגמה "אינו רלוונטי" ל-status='irrelevant').
status: closed / uploaded / irrelevant.
Returns: JSON של הרשומה המעודכנת.
"""
try:
mp_id = UUID(id.strip())
except ValueError:
return _err("id לא תקין")
cl_uuid: UUID | None = None
if linked_case_law_id.strip():
try:
cl_uuid = UUID(linked_case_law_id.strip())
except ValueError:
return _err("linked_case_law_id לא תקין")
status_clean = status.strip() or "closed"
if status_clean not in db.ALLOWED_MP_STATUS:
return _err(
f"status לא תקין. ערכים תקפים: "
f"{', '.join(sorted(db.ALLOWED_MP_STATUS))}"
)
try:
row = await db.close_missing_precedent(
mp_id=mp_id,
linked_case_law_id=cl_uuid,
notes=notes.strip() or None,
status=status_clean,
)
except Exception as e:
return _err(str(e))
if row is None:
return _err("רשומה לא נמצאה")
return _ok(row)