feat(citation-verify): backend for the "אימות פסיקה" panel — per-argument support + verify gate (#154)
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user