"""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())