fix(corpus): re-link orphan citations when a cited ruling is uploaded (G1)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

precedent_internal_citations resolves cited_case_law_id only at extraction
time. When a decision cites a ruling that isn't in the corpus yet, the edge is
stored with cited_case_law_id=NULL. The ruling is often uploaded days later —
but the orphan edge was never re-resolved, so the citation graph permanently
under-counted real connectivity (same stale-derived-value pattern as the
searchable flag). Worse, external rulings store case_number WITHOUT the court
prefix ("3213/97") while citations carry it ("ע\"א 3213/97"), so a large share
of "missing" citations were actually present-but-unlinked.

Fix:
- new citation_extractor.relink_orphan_citations(case_law_id=None): re-resolves
  orphan edges via the canonical _resolve_case_law_id (no parallel matching, G2);
  scoped to one row, or full sweep when None. Idempotent.
- ingest_document calls it on every upload (non-fatal) so the graph self-heals.

Existing backlog already re-linked via a one-off sweep against the live DB:
linked 128 edges → in-corpus distinct rulings 67→178, unlinked distinct
262→143. So real "cited but not uploaded" ≈ 143, not the 262 the stale graph
implied.

Invariants: G1 (re-resolve the derived link at the write) · G2 (single resolver).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 11:46:47 +00:00
parent 473c54fc43
commit f7bf437f67
2 changed files with 50 additions and 0 deletions

View File

@@ -432,3 +432,42 @@ async def get_cited_case_law_ids(source_case_law_ids: list[UUID]) -> dict[str, l
for r in rows:
out.setdefault(r["source_id"], []).append(r["cited_id"])
return out
async def relink_orphan_citations(case_law_id: "UUID | None" = None) -> int:
"""Back-fill ``cited_case_law_id`` on orphan citation edges that now resolve.
Citations are resolved to a corpus row only at extraction time (see
``extract_and_store``). When a cited ruling is uploaded LATER, its existing
orphan edges (``cited_case_law_id IS NULL``) are never re-resolved, so the
citation graph under-counts real connectivity. Call on every precedent
upload (``case_law_id`` set → link only edges that resolve to that row) or as
a one-off sweep (``case_law_id=None`` → re-resolve every orphan). Reuses the
canonical resolver ``_resolve_case_law_id`` (no parallel matching logic, G2);
idempotent — a second run links nothing new.
"""
pool = await db.get_pool()
async with pool.acquire() as conn:
orphans = await conn.fetch(
"SELECT DISTINCT cited_case_number FROM precedent_internal_citations "
"WHERE cited_case_law_id IS NULL AND coalesce(cited_case_number, '') <> ''"
)
linked = 0
for r in orphans:
cited = await _resolve_case_law_id(_normalize_case_number(r["cited_case_number"]))
if cited is None:
continue
if case_law_id is not None and cited != case_law_id:
continue
async with pool.acquire() as conn:
res = await conn.execute(
"UPDATE precedent_internal_citations "
"SET cited_case_law_id = $1, confidence = GREATEST(confidence, 0.90) "
"WHERE cited_case_law_id IS NULL AND cited_case_number = $2",
cited, r["cited_case_number"],
)
try:
linked += int(res.split()[-1])
except (ValueError, IndexError):
pass
return linked

View File

@@ -233,6 +233,17 @@ async def ingest_document(
await db.request_metadata_extraction(case_law_id)
await db.request_halacha_extraction(case_law_id)
await db.recompute_searchable(case_law_id)
# Citations resolve to a corpus row only at extraction time. A ruling
# uploaded AFTER it was first cited leaves orphan edges (NULL link),
# so re-link them to this freshly-ingested row now — the citation
# graph self-heals instead of permanently under-counting (G1). Non-fatal.
try:
from legal_mcp.services import citation_extractor as _ce
relinked = await _ce.relink_orphan_citations(case_law_id)
if relinked:
logger.info("relinked %d orphan citation(s) -> %s", relinked, case_law_id)
except Exception as e: # noqa: BLE001 — relink is best-effort
logger.warning("citation relink failed (non-fatal): %s", e)
await progress("completed", 100,
f"נקלט: {stored_chunks} chunks. חילוץ הלכות ומטא-דאטה ממתינים בתור.")