Files
maigret/utils/selfcheck_triage.py
T

128 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""
Triage report for `maigret --self-check --diagnose` output.
False negatives (claimed user not detected) and check errors are invisible
in normal usage: users just don't see the account. This tool surfaces them.
Workflow:
# 1. Run self-check on a TEMP COPY of the DB (scans/self-check may rewrite it)
cp maigret/resources/data.json /tmp/data.json
echo n | python3 -m maigret --self-check --diagnose --top-sites 500 \
--db /tmp/data.json --no-progressbar --no-color > /tmp/selfcheck.log 2>&1
# 2. Classify and rank failures by site popularity
python3 utils/selfcheck_triage.py /tmp/selfcheck.log /tmp/data.json
"""
import argparse
import json
import re
from pathlib import Path
KINDS = ("FALSE_NEGATIVE", "ERROR", "CONNECT_FAIL", "FALSE_POSITIVE", "OTHER")
def parse_diagnosis_blocks(log_text: str) -> dict:
"""Extract {site_name: [issue, ...]} from --diagnose output."""
blocks = {}
cur = None
for line in log_text.splitlines():
m = re.match(r"^--- (.+) DIAGNOSIS ---$", line.strip())
if m:
cur = m.group(1)
blocks[cur] = []
continue
if cur is None:
continue
s = line.strip()
if s.startswith("- "):
blocks[cur].append(s[2:])
elif s.startswith(("Check type:", "Issues:", "Recommendations:", "->")):
continue
elif s and not line.startswith(" "):
cur = None
return blocks
def classify(issues: list) -> set:
kinds = set()
for i in issues:
if re.search(r"Claimed user .* not detected as claimed", i):
kinds.add("FALSE_NEGATIVE")
elif re.search(r"Unclaimed user .* detected as claimed", i):
kinds.add("FALSE_POSITIVE")
elif i.startswith("Error checking"):
kinds.add("ERROR")
elif "Cannot connect to host" in i:
kinds.add("CONNECT_FAIL")
else:
kinds.add("OTHER")
return kinds
def main():
parser = argparse.ArgumentParser(
description="Classify and rank maigret --self-check --diagnose failures."
)
parser.add_argument("log", type=Path, help="Captured self-check output")
parser.add_argument(
"db",
type=Path,
nargs="?",
default=Path(__file__).parent.parent / "maigret" / "resources" / "data.json",
help="data.json used for the run (for alexaRank ordering)",
)
parser.add_argument(
"--kind",
choices=KINDS,
help="Show only one failure kind",
)
parser.add_argument(
"--limit", type=int, default=40, help="Max sites per kind (default: 40)"
)
args = parser.parse_args()
sites_cfg = json.loads(args.db.read_text())["sites"]
blocks = parse_diagnosis_blocks(args.log.read_text(errors="replace"))
rows = []
for name, issues in blocks.items():
cfg = sites_cfg.get(name, {})
rows.append(
(
cfg.get("alexaRank", 9999999),
name,
classify(issues),
issues,
cfg.get("checkType"),
)
)
rows.sort()
counts = {}
for _, _, kinds, _, _ in rows:
for k in kinds:
counts[k] = counts.get(k, 0) + 1
print(f"Total failing sites: {len(rows)}; kinds: {counts}\n")
shown_kinds = (args.kind,) if args.kind else KINDS
for label in shown_kinds:
subset = [r for r in rows if label in r[2]]
print(f"=== {label} ({len(subset)}) ===")
for rank, name, kinds, issues, ctype in subset[: args.limit]:
extra = "+".join(sorted(kinds - {label}))
extra = f" [{extra}]" if extra else ""
print(f" rank={rank:<8} {name} (checkType={ctype}){extra}")
for i in issues:
print(f" * {i[:160]}")
if len(subset) > args.limit:
print(f" ... and {len(subset) - args.limit} more")
print()
if __name__ == "__main__":
main()