File size: 6,703 Bytes
1d9bd9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/usr/bin/env python3
"""Build corpus_ledger.jsonl — one row of identity + health facts per unique judgment.

Fixes recorded (not applied to the source artifacts — the ledger is a sidecar the
serving layer reads):
  - decision_year vs scr_volume_year (meta `year` is the reporter-volume year; wrong
    for filters/boosts in ~24% of docs)
  - duplicate meta rows (43,175 lines -> 37,898 unique doc_ids; dup_rows counted)
  - text health: stored chars vs official SCR page span (from escr_pdfmap path
    YYYY_VOL_START_END) + garbled-token noise rate
  - bench_n: bench_strength string -> int (the runtime previously did int("division")
    -> always 0, silently killing the bench boost/filter)
  - sibling clusters: referral orders / main judgments / reviews of the same case share
    party names within a few years; cluster them and mark the CANONICAL member (longest
    text) so lookups prefer the judgment over its 5-page order.

Run:  python phase1/scripts/build_ledger.py [data_dir]      (~3 min, CPU only)
Out:  <data_dir>/corpus_ledger.jsonl
"""
import json, os, re, sys, time
from collections import Counter, defaultdict

data_dir = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("THEMIS_DATA", "phase1/data/thor_artifacts")
t0 = time.time()

BENCH_N = {"single": 1, "division": 2, "full": 3, "constitution": 5, "larger": 7}
_STOP = {"the", "of", "and", "state", "union", "india", "others", "ors", "anr", "another",
         "etc", "through", "lrs", "dead", "smt", "shri", "sri", "mst", "dr", "m/s", "vs", "v"}

def party_tokens(name):
    toks = set()
    for side in re.split(r"\bv\.?s?\b|\bversus\b", (name or "").lower())[:2]:
        for w in re.findall(r"[a-z]+", side):
            if len(w) >= 4 and w not in _STOP:
                toks.add(w)
    return toks

# ---------- meta (count dup rows, keep first record per doc_id) ----------
meta, dup_rows = {}, Counter()
for line in open(os.path.join(data_dir, "escr_meta.jsonl"), encoding="utf-8"):
    m = json.loads(line)
    d = m["doc_id"]
    if d in meta: dup_rows[d] += 1
    else: meta[d] = m
print(f"[ledger] meta: {len(meta)} unique docs, {sum(dup_rows.values())} duplicate rows", flush=True)

# ---------- official page spans ----------
span = {}
if os.path.exists(os.path.join(data_dir, "escr_pdfmap.jsonl")):
    for line in open(os.path.join(data_dir, "escr_pdfmap.jsonl"), encoding="utf-8"):
        r = json.loads(line)
        p = r["path"].split("_")
        if len(p) == 4 and p[2].isdigit() and p[3].isdigit():
            pages = int(p[3]) - int(p[2]) + 1
            if 0 < pages < 3000: span[r["doc_id"]] = pages

# ---------- one streaming pass over chunks: chars + noise ----------
chars, noise_num, noise_den = Counter(), Counter(), Counter()
pat = re.compile(r'"doc_id":\s*"([^"]+)"')
mixed = re.compile(r"[a-zA-Z]\d|\d[a-zA-Z]")
ctrl = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f~·]")
ok2 = {"a","an","is","of","to","in","by","on","at","or","as","it","be","we","he","no","so","if","do","us","up"}
for line in open(os.path.join(data_dir, "escr_chunks.jsonl"), encoding="utf-8"):
    d = pat.search(line[:120]).group(1)
    i = line.find('"text":')
    t = line[i + 9:-3]
    chars[d] += len(t)
    toks = t.split()
    if toks:
        bad = 0
        for w in toks:
            if mixed.search(w) or ctrl.search(w): bad += 1
            elif len(w) <= 2 and w.isalpha() and w.lower() not in ok2: bad += 1
        noise_num[d] += bad; noise_den[d] += len(toks)
print(f"[ledger] chunk pass done, {time.time()-t0:.0f}s", flush=True)

# ---------- sibling clustering (blocked by shared distinctive party token) ----------
ptoks = {d: party_tokens(m.get("case_name")) for d, m in meta.items()}
def dyear(d):
    dt = meta[d].get("date") or ""
    return int(dt[:4]) if dt[:4].isdigit() else 0

block = defaultdict(list)
for d, ts in ptoks.items():
    for t in ts: block[t].append(d)

parent = {d: d for d in meta}
def find(x):
    while parent[x] != x:
        parent[x] = parent[parent[x]]; x = parent[x]
    return x
def union(a, b):
    ra, rb = find(a), find(b)
    if ra != rb: parent[rb] = ra

pairs_checked = 0
for t, docs in block.items():
    if len(docs) > 40: continue                       # common token — not distinctive, skip block
    for i in range(len(docs)):
        for j in range(i + 1, len(docs)):
            a, b = docs[i], docs[j]
            if abs(dyear(a) - dyear(b)) > 6: continue
            ta, tb = ptoks[a], ptoks[b]
            if not ta or not tb: continue
            ov = len(ta & tb) / min(len(ta), len(tb))
            if ov >= 0.8:
                union(a, b); pairs_checked += 1

clusters = defaultdict(list)
for d in meta: clusters[find(d)].append(d)
cluster_id, canonical = {}, {}
n_multi = 0
for root, members in clusters.items():
    cid = f"c{abs(hash(root)) % 10**9}" if len(members) > 1 else None
    if len(members) > 1:
        n_multi += 1
        canon = max(members, key=lambda d: chars.get(d, 0))
        for d in members:
            cluster_id[d] = cid; canonical[d] = (d == canon)
print(f"[ledger] sibling clusters: {n_multi} multi-doc clusters "
      f"({sum(len(v) for v in clusters.values() if len(v)>1)} docs), {time.time()-t0:.0f}s", flush=True)

# ---------- write ----------
out = os.path.join(data_dir, "corpus_ledger.jsonl")
n_mismatch = 0
with open(out, "w", encoding="utf-8") as f:
    for d, m in meta.items():
        dy = dyear(d)
        try: vy = int(str(m.get("year") or "")[:4])
        except Exception: vy = 0
        ch = chars.get(d, 0)
        pg = span.get(d)
        cpp = round(ch / pg, 1) if pg else None
        health = "empty" if ch == 0 else ("thin" if (cpp is not None and cpp < 800 and pg >= 5) else "ok")
        mism = bool(dy and vy and dy != vy); n_mismatch += mism
        row = {"doc_id": d, "decision_year": dy or None, "scr_volume_year": vy or None,
               "year_mismatch": mism, "dup_rows": dup_rows.get(d, 0),
               "chars": ch, "official_pages": pg, "chars_per_page": cpp, "text_health": health,
               "held_len": len(str(m.get("held") or "")),
               "noise_rate": round(noise_num.get(d, 0) / noise_den[d], 4) if noise_den.get(d) else None,
               "bench_n": BENCH_N.get(str(m.get("bench_strength") or "").lower(), 0),
               "cluster_id": cluster_id.get(d), "canonical": canonical.get(d, True)}
        f.write(json.dumps(row, ensure_ascii=False) + "\n")

print(f"[ledger] wrote {len(meta)} rows -> {out}", flush=True)
print(f"[ledger] year_mismatch: {n_mismatch} ({n_mismatch/len(meta):.1%}) | "
      f"empty: {sum(1 for d in meta if chars.get(d,0)==0)} | "
      f"dup rows total: {sum(dup_rows.values())} | {time.time()-t0:.0f}s", flush=True)