Files
legal-ai/scripts/classify_citation_treatments.py
Chaim ccc5a73bc8
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
feat(x11): treatment-aware citation authority wired into research agents (#154)
The internal citation graph fed only RANKING (raw in-degree), and the per-citation
TREATMENT was never classified — so a precedent distinguished N times got the same
authority boost as one followed N times (INV-COR2 violation), and the signal never
reached the agents' reasoning. Wires the full path:

Phase 1 — scripts/classify_citation_treatments.py: classify each linked edge's
  treatment (followed/distinguished/…) from its match_context via
  corroboration.classify_treatment (Opus 4.8 @ xhigh, local), filling
  precedent_internal_citations.treatment. Idempotent.
Phase 2 — db.refresh_verified_layer: count only NON-negative treatments toward
  verified/cite_count (INV-COR2/COR4). Unclassified counts as neutral-positive so
  the signal degrades gracefully before classification runs.
Phase 3 — db.citation_authority(ids): per-precedent {total, positive, negative,
  unclassified, by_treatment}. Surfaced as `cited_by` in search_precedent_library
  hits and precedent_library_get, and `treatment` per incoming citation.
Phase 4 — legal-researcher/analyst/writer prompts: weigh & ARGUE authority
  ("הלכה שאומצה ב-N החלטות ועדת-ערר"), flag distinguished/overruled, never invent
  the count (INV-AH; writer is read-only of the analyst).

Auto-approval stays kill-switched off (chair gate preserved, INV-G10). No schema
change (treatment column already existed). Operational: run the classifier +
refresh_verified_layer over the 379 edges, then sync agents across companies.

Invariants: G2 (one classifier + one authority query, reused), INV-COR2/COR3/COR4
(negative never corroborates; point-specific; ≥N), INV-G10 (no auto-approval),
INV-AH (no invented numbers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:04:04 +00:00

100 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Classify the TREATMENT of each internal citation edge (X11 Phase 2, #154).
Each row in ``precedent_internal_citations`` records that a committee decision
cited a precedent, with the surrounding ``match_context``. Until now the edge's
``treatment`` column was empty, so the verified/authority layer counted every
citation as if it were positive — a precedent *distinguished* N times got the
same authority boost as one *followed* N times (an INV-COR2 violation).
This script fills ``treatment`` per edge by classifying the context with
``corroboration.classify_treatment`` (Opus 4.8 @ xhigh via the local
claude_session bridge — LOCAL ONLY, the claude CLI is not in the container):
followed | explained → positive (counts toward authority)
distinguished | criticized |
questioned | overruled → negative (never counts; overruled = demote)
Scope: only LINKED edges (``cited_case_law_id IS NOT NULL``) with an empty
``treatment`` and a non-empty ``match_context``. Idempotent — a second run skips
rows already classified. After applying, run ``scripts/build_verified_layer.py``
(or ``db.refresh_verified_layer``) so the treatment-aware count takes effect.
Run (dry-run, default — classifies and PRINTS, writes nothing):
HOME=/home/chaim mcp-server/.venv/bin/python scripts/classify_citation_treatments.py
Apply:
HOME=/home/chaim mcp-server/.venv/bin/python scripts/classify_citation_treatments.py --apply
Options:
--limit N classify at most N edges (smoke test)
--case-law-id UUID restrict to citations TO this one precedent
"""
from __future__ import annotations
import argparse
import asyncio
import os
import sys
from uuid import UUID
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src"))
from legal_mcp.services import corroboration, db # noqa: E402
async def _pending(limit: int | None, case_law_id: str | None) -> list[dict]:
pool = await db.get_pool()
where = ["cited_case_law_id IS NOT NULL", "coalesce(treatment,'') = ''",
"coalesce(match_context,'') <> ''"]
params: list = []
if case_law_id:
params.append(UUID(case_law_id))
where.append(f"cited_case_law_id = ${len(params)}")
sql = (f"SELECT id, cited_case_number, match_context "
f"FROM precedent_internal_citations WHERE {' AND '.join(where)} "
f"ORDER BY created_at")
if limit:
sql += f" LIMIT {int(limit)}"
rows = await pool.fetch(sql, *params)
return [dict(r) for r in rows]
async def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--apply", action="store_true", help="write changes (default: dry-run)")
ap.add_argument("--limit", type=int, default=None)
ap.add_argument("--case-law-id", type=str, default=None)
args = ap.parse_args()
rows = await _pending(args.limit, args.case_law_id)
print(f"קצוות לא-מסווגים לעיבוד: {len(rows)}\n")
pool = await db.get_pool()
counts: dict[str, int] = {}
errors = 0
for r in rows:
try:
t = await corroboration.classify_treatment(
r["cited_case_number"] or "", r["match_context"] or "")
except Exception as e: # noqa: BLE001 — one bad row must not abort the batch
errors += 1
print(f" ✗ [error] {r['cited_case_number']}: {type(e).__name__}: {e}")
continue
counts[t] = counts.get(t, 0) + 1
sign = "" if corroboration.is_positive(t) else ("" if corroboration.is_negative(t) else "·")
print(f" {sign} {t:<14} {r['cited_case_number']}")
if args.apply:
await pool.execute(
"UPDATE precedent_internal_citations SET treatment = $2 WHERE id = $1",
r["id"], t,
)
print(f"\nסיכום טיפול: {counts} שגיאות={errors}"
+ ("" if args.apply else " (dry-run — לא נכתב)"))
if args.apply:
print("הרץ עכשיו: scripts/build_verified_layer.py (או db.refresh_verified_layer) "
"כדי שהספירה מודעת-הטיפול תיכנס לתוקף.")
if __name__ == "__main__":
asyncio.run(main())