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>
100 lines
4.1 KiB
Python
100 lines
4.1 KiB
Python
"""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())
|