| """CP5 + Stage-2 — deterministic good-law citator AND citation-graph builder (no DeepSeek). |
| |
| Tier-1 (good-law): scan each judgment for explicit overruling/doubting DECLARATIONS, resolve the |
| named case via the citation index, record a NEGATIVE edge (bench + date), derive status by latching |
| on the strongest *competent* negative (fail-safe to 'doubted'; default 'unknown'; never good-law from |
| absence). |
| |
| Stage-2 (the citation GRAPH that feeds search): harvest every inbound edge and PERSIST it. |
| - cite edges: in-text reporter citations (expanded regex: bare/no-vol SCR, Supp. volumes, year-first |
| AIR), resolved cross-reporter via parallel-citation union-find. |
| - NAME edges: cases cited BY NAME ("Khushal Rao v State of Bombay") — the dominant way foundational |
| cases are referenced (Khushal Rao: graph cited_by 0 via regex, but named in ~39 bodies). Resolved |
| via a distinctive-party name index. THIS is the foundational-recall fix. |
| - dedup by (from,target); a body cite that resolves to NO corpus doc is dropped, never a dangling edge. |
| |
| In: escr_corpus_full.jsonl |
| Out: good_law.jsonl (doc_id, status, provenance, as_of, cited_by, treatment_breakdown) |
| edges.jsonl (from, target, treatment, method, para) <- the graph |
| Run on Thor: CORPUS=escr_corpus_full.jsonl python3 21_citator.py |
| """ |
| import json, os, re |
| from collections import defaultdict, Counter |
|
|
| CORPUS = os.getenv("CORPUS", "escr_corpus_full.jsonl") |
| OUT = os.getenv("OUT", "good_law.jsonl") |
| EDGES_OUT = os.getenv("EDGES_OUT", "edges.jsonl") |
|
|
| BENCH_RANK = {"single": 1, "division": 2, "full": 3, "constitution": 5, "larger": 7} |
| def rank(b): |
| if b is None: return None |
| if isinstance(b, int): return b |
| if b in BENCH_RANK: return BENCH_RANK[b] |
| m = re.match(r"\d+", str(b)); return int(m.group()) if m else None |
|
|
| NEG_PATTERNS = [ |
| (re.compile(r"\b(?:is|are|stand[s]?|hereby|hereby\s+)?\s*overrul(?:e|ed|es|ing)\b", re.I), "overruled"), |
| (re.compile(r"\bwe\s+overrule\b", re.I), "overruled"), |
| (re.compile(r"\bno longer\s+(?:good law|the law|holds the field|holds good)\b", re.I), "overruled"), |
| (re.compile(r"\b(?:cannot|can no longer)\s+be\s+(?:considered|treated|regarded)\s+(?:as\s+)?good law\b", re.I), "overruled"), |
| (re.compile(r"\bdoes not (?:lay down|state) the correct law\b", re.I), "overruled"), |
| (re.compile(r"\b(?:partly|partially)\s+overrul", re.I), "partly_overruled"), |
| (re.compile(r"\bper incuriam\b", re.I), "per_incuriam"), |
| (re.compile(r"\bdoubt(?:ed|s)?\s+the correctness\b", re.I), "doubted"), |
| (re.compile(r"\bcorrectness\s+(?:of|.{0,40}?)\s+(?:is\s+)?doubt", re.I), "doubted"), |
| (re.compile(r"\breferred?\s+to\s+a\s+larger\s+bench\b", re.I), "doubted"), |
| ] |
| SEVERITY = {"overruled": 4, "partly_overruled": 3, "per_incuriam": 3, "doubted": 2} |
| POS_LEX = [("relied", "relied_on"), ("followed", "followed"), ("approved", "approved"), |
| ("affirmed", "affirmed"), ("reiterated", "followed"), ("distinguish", "distinguished"), |
| ("referred", "referred")] |
| TREAT_PRI = {"relied_on": 5, "followed": 4, "approved": 4, "affirmed": 4, |
| "distinguished": 3, "referred": 2, "cited": 1, "named": 1} |
|
|
| |
| _C = (r"\[?\d{4}\]?\s*(?:supp\.?\s*)?\d*\s*S\.?C\.?R\.?\s*\d+" |
| r"|\(\d{4}\)\s*(?:supp\.?\s*)?\d+\s*SCC\s*\d+" |
| r"|AIR\s+\d{4}\s+SC\s+\d+|\d{4}\s+AIR\s+(?:SC\s+)?\d+|\d{4}\s+INSC\s+\d+") |
| CITE = re.compile(_C, re.I) |
| ONE = r"(?:" + _C + r")" |
| PARALLEL = re.compile(ONE + r"(?:\s*[:;]\s*" + ONE + r")+", re.I) |
|
|
| def norm_cite(c): return re.sub(r"\s+", " ", (c or "").replace(".", "")).strip().upper() |
|
|
| |
| NAME_STOP = set("v vs versus of the and in re m s smt sri shri dr ms mr kumari ors anr etc another others " |
| "state union india govt government through rep by its secretary ministry law justice department " |
| "maharashtra punjab gujarat rajasthan bombay delhi kerala karnataka bihar uttar pradesh madhya " |
| "tamil nadu andhra telangana bengal west haryana assam odisha orissa jharkhand chhattisgarh " |
| "himachal uttarakhand goa manipur tripura nagaland mizoram sikkim meghalaya nct calcutta madras " |
| "allahabad company ltd limited pvt private corporation board authority commissioner".split()) |
| def distinctive_key(party): |
| toks = [t for t in re.findall(r"[a-z]+", (party or "").lower()) if t not in NAME_STOP and len(t) >= 3] |
| if not toks: return None |
| if len(toks) >= 2 or len(toks[0]) >= 7: |
| return " ".join(toks[:4]) |
| return None |
| |
| |
| NAMECITE = re.compile( |
| r"(?:\bin|\bsee|\bper|\bfollowing|\breiterated in|\breaffirmed in|\brelied (?:up)?on(?: in)?|" |
| r"\bdecisions?\s+in|\bjudgments?\s+in|\bheld\s+in|\blaid\s+down\s+in|\bobserved\s+in|\bcase\s+of|\bratio\s+(?:in|of))\s+" |
| r"([A-Z][A-Za-z.&'’\-]+(?:\s+[A-Z][A-Za-z.&'’\-]+){0,4})\s+v(?:s|ersus)?\.?\s+" |
| r"([A-Z][A-Za-z.&'’\-]+(?:\s+[A-Za-z.&'’\-]+){0,3})", re.I) |
| def second_party(nm): |
| p = re.split(r"\s+v[s.]?\s+|\s+versus\s+", nm or "", 1, flags=re.I) |
| return p[1] if len(p) > 1 else "" |
| def party_tokens(s): return {t for t in re.findall(r"[a-z]+", (s or "").lower()) if t not in NAME_STOP and len(t) >= 4} |
|
|
| print("loading corpus + building indexes...", flush=True) |
| docs, seen_ids = [], set() |
| cite2doc = {} |
| for l in open(CORPUS): |
| r = json.loads(l); did = r.get("doc_id") |
| if did in seen_ids: continue |
| seen_ids.add(did); docs.append(r) |
| for key in ([r.get("neutral_citation")] + (r.get("equivalent_citations") or [])): |
| if key: cite2doc[norm_cite(key)] = did |
| by_id = {r.get("doc_id"): r for r in docs} |
|
|
| keydocs = defaultdict(list) |
| for r in docs: |
| first = re.split(r"\s+v[s.]?\s+|\s+versus\s+", r.get("case_name") or "", 1, flags=re.I)[0] |
| k = distinctive_key(first) |
| if k: keydocs[k].append(r.get("doc_id")) |
| print(f"{len(docs)} docs (deduped), {len(cite2doc)} citation keys, {len(keydocs)} name keys", flush=True) |
|
|
| |
| |
| |
| |
| cite2doc_base = dict(cite2doc) |
|
|
| |
| parent = {} |
| def _year(c): |
| m = re.search(r"\d{4}", c or ""); return m.group() if m else None |
| def _find(x): |
| parent.setdefault(x, x) |
| while parent[x] != x: parent[x] = parent[parent[x]]; x = parent[x] |
| return x |
| def _union(a, b): parent[_find(a)] = _find(b) |
| def _group(cites): |
| cs = [norm_cite(c) for c in cites if c] |
| for c in cs[1:]: _union(cs[0], c) |
| for r in docs: |
| for m in PARALLEL.finditer(r.get("full_text") or ""): |
| cs = re.split(r"\s*[:;]\s*", m.group(0)) |
| byy = defaultdict(list) |
| for c in cs: |
| y = _year(c) |
| if y: byy[y].append(c) |
| for grp in byy.values(): _group(grp) |
| for ed in (r.get("cases_cited") or []): |
| _group(ed.get("citations") or []) |
| _group([r.get("neutral_citation")] + (r.get("equivalent_citations") or [])) |
| groups = defaultdict(list) |
| for c in list(parent): groups[_find(c)].append(c) |
| added = 0 |
| for g in groups.values(): |
| doc = next((cite2doc[c] for c in g if c in cite2doc), None) |
| if doc: |
| for c in g: |
| if c not in cite2doc: cite2doc[c] = doc; added += 1 |
| print(f"cross-reporter enrichment: +{added} keys -> {len(cite2doc)} total", flush=True) |
|
|
| def resolve_neg(ft, vs, ve, radius=70): |
| """Tier-1 negative-edge targeting — UNIQUENESS guard (precision over recall). The overruled case |
| must be the SINGLE case unambiguously tied to the verb: collect every resolvable target within a |
| tight window (base citation OR distinctive case-name) and flag it ONLY if exactly one distinct doc |
| qualifies. Ambiguous (a good-law case cited near the overruled one, e.g. Maneka near ADM Jabalpur |
| in Puttaswamy) or none -> attribute NOTHING. Asymmetric cost: a false overruling is catastrophic; |
| a missed one defaults to honest 'unknown' (which now renders as nothing).""" |
| |
| overruler_after = re.match(r"\s*(?:in|by)\b", ft[ve: ve + 40].lower()) |
| base = max(0, vs - radius) |
| seg = ft[base: ve + radius] |
| targets = set() |
| def consider(pos, d): |
| if not d: return |
| if overruler_after and pos >= ve: return |
| targets.add(d) |
| for m in CITE.finditer(seg): |
| consider(base + m.start(), cite2doc_base.get(norm_cite(m.group(0)))) |
| return (next(iter(targets)), None) if len(targets) == 1 else (None, None) |
|
|
| def window_treatment(win): |
| w = win.lower() |
| for kw, lab in POS_LEX: |
| if kw in w: return lab |
| return "cited" |
|
|
| |
| edges = {} |
| def add_edge(frm, tgt, tre, method, para): |
| if not tgt or frm == tgt: return |
| k = (frm, tgt); cur = edges.get(k) |
| if cur is None: |
| edges[k] = {"treatment": tre, "method": method, "para": para[:160]} |
| else: |
| if TREAT_PRI.get(tre, 0) > TREAT_PRI.get(cur["treatment"], 0): cur["treatment"] = tre |
| if cur["method"] == "name" and method == "cite": cur["method"] = "cite"; cur["para"] = para[:160] |
|
|
| neg_edges = defaultdict(list) |
| cite_indeg = Counter() |
| n_decl = n_name = 0 |
| |
| for r in docs: |
| ft = r.get("full_text") or "" |
| self_id = r.get("doc_id") |
| cb, cd = rank(r.get("bench_strength")), r.get("date") |
| for pat, kind in NEG_PATTERNS: |
| for m in pat.finditer(ft): |
| tgt, c = resolve_neg(ft, m.start(), m.end()) |
| if tgt and tgt != self_id: |
| neg_edges[tgt].append({"from": self_id, "from_cite": r.get("neutral_citation"), |
| "kind": kind, "bench": cb, "date": cd, |
| "passage": re.sub(r"\s+", " ", ft[max(0, m.start() - 170): m.end() + 170])}); n_decl += 1 |
| for m in CITE.finditer(ft): |
| tgt = cite2doc.get(norm_cite(m.group(0))) |
| if not tgt or tgt == self_id: continue |
| win = ft[max(0, m.start() - 120): m.end() + 120] |
| add_edge(self_id, tgt, window_treatment(win), "cite", re.sub(r"\s+", " ", win)) |
| cite_indeg[tgt] += 1 |
|
|
| |
| name_index = {k: (ds[0] if len(ds) == 1 else max(ds, key=lambda d: (cite_indeg.get(d, 0), d))) for k, ds in keydocs.items()} |
|
|
| |
| for r in docs: |
| ft = r.get("full_text") or "" |
| self_id = r.get("doc_id") |
| for m in NAMECITE.finditer(ft): |
| k = distinctive_key(m.group(1)); tgt = name_index.get(k) if k else None |
| if not tgt or tgt == self_id: continue |
| cs = party_tokens(m.group(2)); ts = party_tokens(second_party(by_id.get(tgt, {}).get("case_name"))) |
| if cs and ts and not (cs & ts): continue |
| win = ft[max(0, m.start() - 60): m.start() + 200]; tre = window_treatment(win) |
| add_edge(self_id, tgt, tre if tre != "cited" else "named", "name", re.sub(r"\s+", " ", win)); n_name += 1 |
|
|
| cited_by = defaultdict(dict) |
| for (frm, tgt), e in edges.items(): |
| cited_by[tgt][frm] = e["treatment"] |
| print(f"tier-1 neg edges: {n_decl} | total graph edges: {len(edges)} (name-mentions scanned: {n_name})", flush=True) |
|
|
| def derive(did): |
| tgt = by_id[did]; tb = rank(tgt.get("bench_strength")); td = tgt.get("date") |
| best = None |
| for e in neg_edges.get(did, []): |
| if td and e["date"] and e["date"] <= td: continue |
| competent = (e["bench"] is not None and tb is not None and e["bench"] >= tb) |
| sev = SEVERITY.get(e["kind"], 0) |
| if best is None or sev > best[0]: best = (sev, e["kind"], e, competent) |
| if best is None: |
| return {"good_law_status": "unknown", "provenance": "no negative treatment found", "as_of": None} |
| sev, kind, e, competent = best; asof = e["date"] |
| extra = {"passage": e.get("passage", ""), "overruling": e.get("from"), "overruling_cite": e.get("from_cite")} |
| if kind == "doubted": |
| return {"good_law_status": "doubted", "provenance": f"doubted by {e['from_cite']}", "as_of": asof, **extra} |
| if kind == "per_incuriam": |
| return {"good_law_status": "per_incuriam", "provenance": f"held per incuriam in {e['from_cite']}", "as_of": asof, **extra} |
| if not competent: |
| return {"good_law_status": "doubted", "provenance": f"negative treatment in {e['from_cite']} — overruling bench not confirmed >= target", |
| "as_of": asof, "needs_review": True, **extra} |
| return {"good_law_status": kind, "provenance": f"{kind} by {e['from_cite']} (bench {e['bench']} >= {tb})", "as_of": asof, **extra} |
|
|
| status_counts = Counter(); nonzero = 0 |
| with open(OUT, "w") as f: |
| for r in docs: |
| did = r.get("doc_id"); gl = derive(did); cb = cited_by.get(did, {}) |
| nonzero += 1 if cb else 0 |
| rec = {"doc_id": did, "neutral_citation": r.get("neutral_citation"), "case_name": r.get("case_name"), **gl, |
| "cited_by": len(cb), "treatment_breakdown": dict(Counter(cb.values()))} |
| f.write(json.dumps(rec, ensure_ascii=False) + "\n") |
| status_counts[gl["good_law_status"]] += 1 |
|
|
| with open(EDGES_OUT, "w") as f: |
| for (frm, tgt), e in edges.items(): |
| f.write(json.dumps({"from": frm, "target": tgt, **e}, ensure_ascii=False) + "\n") |
|
|
| print("status distribution:", dict(status_counts), flush=True) |
| print(f"cited_by nonzero: {nonzero}/{len(docs)} ({100*nonzero//len(docs)}%)", flush=True) |
| print(f"wrote {OUT} and {EDGES_OUT} ({len(edges)} edges)", flush=True) |
|
|