-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathidentity_healthcheck.py
More file actions
143 lines (126 loc) · 7.46 KB
/
Copy pathidentity_healthcheck.py
File metadata and controls
143 lines (126 loc) · 7.46 KB
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
#!/usr/bin/env python3
"""Pre-launch player-identity health check for a tour.
Run before a tour goes live in ANY app (auction / draft / points feed):
python3 identity_healthcheck.py "lanka premier league"
Exit 1 on any BLOCKER, so a setup step / CI can gate on it.
What it catches (and how reliable it is with LOCAL data only):
BLOCKER dup-cricsheet one cricsheet_id under >1 registry pid — identity
merge/split corruption. Fully reliable.
BLOCKER fixable-miss a squad name with NO cricsheet_id for which a confident,
given-name-compatible DB record WITH match data exists
(the Sam Harper / Asitha class — a real record we failed
to link). Actionable: add a bridge. Reliable for
SAME-surname misses.
INFO unmapped no cricsheet_id and no findable record — genuinely
uncapped (born-2004 U19 types). Expected; triage once.
REVIEW name-mismatch anchored matches whose go-by name isn't derivable from
the cricsheet initials. NOTE: this is mostly LEGIT for SL
players (Kusal Mendis = BKG Mendis) — local data has no
full/common name, so this can't be auto-verified. Scan it
by eye for a genuine wrong-namesake (Dale Phillips linked
to GD/Glenn Phillips). Definitive detection needs an ESPN
key_cricinfo full-name cross-ref (people.csv has the id) —
a future enhancement. Not a blocker.
Reuses build_registry's matcher, so this check and the registry build never drift.
"""
import sys, os, json, sqlite3
from collections import defaultdict
import build_registry as br
def main():
filt = (sys.argv[1].lower() if len(sys.argv) > 1 else None)
tours = json.load(open(os.path.join(br.HERE, "tours.json")))
con = br.open_pool_con() # live auction DB locally; committed players export in CI
players = br.load_global()
cs_pids = defaultdict(list)
for pid, e in players.items():
if e.get("cricsheet_id"):
cs_pids[e["cricsheet_id"]].append(pid)
dups = {cs: pids for cs, pids in cs_pids.items() if len(pids) > 1}
# SPLIT IDENTITY — the mirror image of dup-cricsheet, and previously unchecked.
# dup-cricsheet catches "one person's cricsheet id under many pids". This catches the opposite:
# ONE PERSON spread across several pids. It matters because the draft stamps a single pid per
# player, so whichever entry it picked is the only one that joins — the other's rows silently
# score 0 or land on the wrong player.
# Found via draft_id (the draft's own player key): two registry entries sharing one draft_id
# are, by the draft's own definition, one player. Real case: Dale Phillips held BOTH ci:902447
# (cricsheet DN Phillips — the LPL player) and ci:823509 (which also carried "gd phillips",
# Glenn Phillips' initials form), both tagged draft_id 10341.
draft_pids = defaultdict(list)
for pid, e in players.items():
d = e.get("draft_id")
if d:
draft_pids[d].append(pid)
split_ids = {d: pids for d, pids in draft_pids.items() if len(pids) > 1}
# match_performances lives ONLY in the full auction DB (not the committed players export used
# in CI). When absent, fixable-miss detection (which needs "this record HAS stats") is skipped
# — advisory only, so degrade to an empty set rather than crash. build_registry still anchors.
try:
have_data = {r[0] for r in con.execute("SELECT DISTINCT player_id FROM match_performances")}
except Exception as e:
print(f" (no match_performances — fixable-miss detection skipped, advisory only: {e})",
file=sys.stderr)
have_data = set()
blockers = 0
for t in tours:
if filt and filt not in t["name"].lower():
continue
spath = os.path.join(br.HERE, t["squads"]) if t.get("squads") else None
if not spath or not os.path.exists(spath):
continue
squad = br.squad_players(spath)
pool = br.db_pool(con, t.get("gender", "female"))
pool_data = [r for r in pool if r["id"] in have_data] # records that actually have stats
cs_name = {r["cricsheet_id"]: r["name"] for r in pool if r.get("cricsheet_id")}
fixable, unmapped, review = [], [], []
for short, tfull, sname, role in squad:
ns = br.norm(sname)
e = next((pe for pe in players.values() if ns in pe.get("aliases", [])), {})
cs = e.get("cricsheet_id")
if cs:
db = cs_name.get(cs)
if db and not br.given_compatible(sname, db):
review.append((short, sname, db))
else:
# ONLY flag an EXACT-normalized-name record that has data but isn't anchored
# (rock-solid + actionable). Fuzzy same-surname "candidates" are NOT flagged —
# that heuristic proposed wrong links (Traveen Mathews -> AD/Angelo Mathews) and
# would repeat the very namesake bug. Fuzzy/surname-hidden misses need the
# web/full-name step, surfaced as unmapped for human triage.
exact = next((r for r in pool_data if br.norm(r["name"]) == ns), None)
if exact:
fixable.append((short, sname, exact["name"], 100))
else:
unmapped.append(sname)
print(f"\n=== {t['name']} — identity health ===")
print(f" squad {len(squad)} | fixable-miss {len(fixable)} | unmapped {len(unmapped)} | name-mismatch review {len(review)}")
for short, s, db, sc in fixable:
print(f" BLOCKER fixable-miss : {short:4} {s:26} -> real record {db!r} WITH data exists (score {sc:.0f}); add a bridge")
if unmapped:
print(f" INFO unmapped : {', '.join(unmapped)}")
if review:
print(f" REVIEW name-mismatch (eyeball — mostly legit SL initials-forms; look for a wrong namesake):")
for short, s, db in review:
print(f" {short:4} {s:26} -> {db}")
blockers += len(fixable)
if dups:
print("\n=== GLOBAL — duplicate cricsheet_id (identity corruption) ===")
for cs, pids in dups.items():
print(f" BLOCKER dup-cricsheet: {cs} under {pids}")
blockers += len(dups)
if split_ids:
print("\n=== GLOBAL — split identity: one draft player under >1 registry pid ===")
print(" The draft stamps ONE pid per player, so only one of these ever joins the sheet;")
print(" the other's rows score 0 or attach to the wrong person. Decide which cricinfo id")
print(" is correct (check cricsheet's initials form for the tour), then merge/split in the")
print(" registry — do NOT leave both live.")
for d, pids in sorted(split_ids.items()):
for pid in pids:
e = players[pid]
print(f" BLOCKER split-identity: draft_id {d} -> {pid:14} "
f"{e.get('display', '?'):24} cs={e.get('cricsheet_id')} "
f"aliases={e.get('aliases')}")
blockers += len(split_ids)
print(f"\n{'FAIL' if blockers else 'PASS'}: {blockers} blocker(s). (review/unmapped are human-triage, not blockers)")
sys.exit(1 if blockers else 0)
if __name__ == "__main__":
main()