feat(citation-verify): backend for the "אימות פסיקה" panel — per-argument support + verify gate (#154)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 12s

Backend half of the citation-verification tab (frontend follows in a separate PR):

- Schema V44: case_precedents gains argument_id (the legal argument it supports),
  case_law_id (the corpus ruling — for cited_by + dedup), verified (the INV-AH gate
  the writer respects) + verified_at. All nullable; chair_note already existed.
- db: create_case_precedent(+argument_id/case_law_id/verified),
  set_case_precedent_verified(id, verified, chair_note), list_case_precedents(+cols).
- services/case_citation_verification.build_view(case): per legal_argument →
  in-corpus suggestions (search_library per issue) each with the cited_by authority
  breakdown (db.citation_authority, X11) + merged attach/verify state + per-issue
  radar (case_digest_radar grouped by matched issue). Pure read/assembly; reuses the
  one corpus search + one authority query + one radar (G2).
- endpoints: GET /api/cases/{n}/citation-verification (the view) and
  POST .../verify (upsert verify/un-verify + chair note).

Validated on 1044-03-26: 14 arguments, all with support; 317/10 surfaces with
אומץ×11/אובחן×1; radar leads attributed per issue. Auto-approval untouched (chair
gate, INV-G10). Verify defaults to false — nothing is authoritative until the chair
marks it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:08:21 +00:00
parent 33c10e4147
commit 5ede8a9653
3 changed files with 243 additions and 5 deletions

View File

@@ -1731,6 +1731,23 @@ UPDATE case_law SET district = 'חיפה'
"""
SCHEMA_V44_SQL = """
-- Citation-verification panel (#154): the chair verifies each corpus precedent
-- against the specific legal ARGUMENT before the writer may cite it (INV-AH gate).
-- case_precedents gains: the argument it supports, the resolved case_law row
-- (for the cited_by authority signal + dedup), a verified flag (the gate), and a
-- verification timestamp. chair_note already exists. All nullable so legacy
-- section-scoped rows stay valid.
ALTER TABLE case_precedents ADD COLUMN IF NOT EXISTS argument_id UUID
REFERENCES legal_arguments(id) ON DELETE SET NULL;
ALTER TABLE case_precedents ADD COLUMN IF NOT EXISTS case_law_id UUID
REFERENCES case_law(id) ON DELETE SET NULL;
ALTER TABLE case_precedents ADD COLUMN IF NOT EXISTS verified BOOLEAN DEFAULT false;
ALTER TABLE case_precedents ADD COLUMN IF NOT EXISTS verified_at TIMESTAMPTZ;
CREATE INDEX IF NOT EXISTS idx_case_precedents_argument ON case_precedents(argument_id);
"""
# Stable, arbitrary key for the session-level advisory lock that serialises
# schema DDL across processes. Every short-lived process (cron drains, services)
# re-runs the idempotent migrations on startup; without this lock two processes
@@ -1796,6 +1813,7 @@ async def _apply_schema_ddl(conn: asyncpg.Connection) -> None:
await conn.execute(SCHEMA_V41_SQL)
await conn.execute(SCHEMA_V42_SQL)
await conn.execute(SCHEMA_V43_SQL)
await conn.execute(SCHEMA_V44_SQL)
async def init_schema() -> None:
@@ -3331,28 +3349,58 @@ async def create_case_precedent(
chair_note: str = "",
pdf_document_id: UUID | None = None,
practice_area: str | None = None,
argument_id: UUID | None = None,
case_law_id: UUID | None = None,
verified: bool = False,
) -> dict:
"""Insert a new precedent attached to a case."""
"""Insert a new precedent attached to a case.
``argument_id``/``case_law_id``/``verified`` (X11 #154) link the attachment to
the specific legal argument it supports and the corpus ruling, and mark whether
the chair verified it (the INV-AH gate the writer respects)."""
pool = await get_pool()
row = await pool.fetchrow(
"""
INSERT INTO case_precedents
(case_id, section_id, quote, citation, chair_note, pdf_document_id, practice_area)
VALUES ($1, $2, $3, $4, $5, $6, $7)
(case_id, section_id, quote, citation, chair_note, pdf_document_id,
practice_area, argument_id, case_law_id, verified, verified_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
CASE WHEN $10 THEN now() ELSE NULL END)
RETURNING *
""",
case_id, section_id, quote, citation, chair_note, pdf_document_id, practice_area,
case_id, section_id, quote, citation, chair_note, pdf_document_id,
practice_area, argument_id, case_law_id, verified,
)
return dict(row)
async def set_case_precedent_verified(
precedent_id: UUID, verified: bool, chair_note: str | None = None,
) -> dict | None:
"""Toggle the chair-verification gate on an attached precedent (#154), optionally
updating the chair note. Sets verified_at when verifying, clears it when un-verifying."""
pool = await get_pool()
sets = ["verified = $2", "verified_at = CASE WHEN $2 THEN now() ELSE NULL END",
"updated_at = now()"]
params: list = [precedent_id, verified]
if chair_note is not None:
sets.append(f"chair_note = ${len(params) + 1}")
params.append(chair_note)
row = await pool.fetchrow(
f"UPDATE case_precedents SET {', '.join(sets)} WHERE id = $1 RETURNING *",
*params,
)
return dict(row) if row else None
async def list_case_precedents(case_id: UUID) -> list[dict]:
"""List all precedents attached to a case, ordered by section then creation time."""
pool = await get_pool()
rows = await pool.fetch(
"""
SELECT id, case_id, section_id, quote, citation, chair_note,
pdf_document_id, practice_area, created_at, updated_at
pdf_document_id, practice_area, argument_id, case_law_id,
verified, verified_at, created_at, updated_at
FROM case_precedents
WHERE case_id = $1
ORDER BY section_id NULLS LAST, created_at