1"""Verify every intended reference against Crossref (public API, polite pool) and build the verified bibliography.23For each seed (author words, title, year) we query Crossref, keep the best candidate only if the title similarity is4high and the year matches (±1), and record DOI, container title, volume, pages. Unverified entries are listed in5literature/unverified.csv and are NOT cited in the paper.6"""7import time, re, json, requests, yaml, pandas as pd8from rapidfuzz import fuzz9from src.common import ROOT, get_logger, ledger1011log = get_logger("crossref")12UA = "WP37-UQO-research/0.1 (mailto:simon-pierre.boucher@uqo.ca)"13LIT = ROOT / "literature"1415def norm(s): return re.sub(r"[^a-z0-9 ]", " ", (s or "").lower())1617def query(author, title, year):18 params = {"query.bibliographic": f"{author} {title} {year}", "rows": 5, "select": "DOI,title,author,issued,container-title,volume,issue,page,type,publisher"}19 r = requests.get("https://api.crossref.org/works", params=params, headers={"User-Agent": UA}, timeout=60)20 r.raise_for_status()21 return r.json()["message"]["items"]2223def main():24 seeds = yaml.safe_load((LIT / "seed_references.yaml").read_text())25 rows = []26 for s in seeds:27 if not s["author"]:28 rows.append({**s, "verified": False, "note": "placeholder topic query, needs a specific paper"}); continue29 try:30 items = query(s["author"], s["title"], s["year"])31 except Exception as e:32 rows.append({**s, "verified": False, "note": f"crossref error {type(e).__name__}"}); continue33 best, best_sim = None, 034 for it in items:35 t = " ".join(it.get("title") or [])36 sim = fuzz.token_set_ratio(norm(t), norm(s["title"])) / 10037 yr = (it.get("issued", {}).get("date-parts") or [[None]])[0][0]38 fam = " ".join(a.get("family", "") for a in it.get("author", []) or [])39 auth_ok = all(norm(w) in norm(fam) for w in s["author"].split()[:1])40 if sim > best_sim and yr and abs(int(yr) - int(s["year"])) <= 1 and auth_ok:41 best, best_sim = it, sim42 if best and best_sim >= 0.90:43 yr = best["issued"]["date-parts"][0][0]44 rows.append({**s, "verified": True, "doi": best["DOI"], "crossref_title": " ".join(best.get("title") or []),45 "journal": " ".join(best.get("container-title") or []), "volume": best.get("volume"), "issue": best.get("issue"),46 "pages": best.get("page"), "pub_year": yr, "type": best.get("type"), "publisher": best.get("publisher"),47 "authors": "; ".join(f"{a.get('family','')}, {a.get('given','')}" for a in best.get("author", []) or []),48 "title_sim": round(best_sim, 3), "note": ""})49 else:50 rows.append({**s, "verified": False, "note": f"no confident Crossref match (best sim {best_sim:.2f})"})51 time.sleep(0.3)52 df = pd.DataFrame(rows)53 df.to_csv(LIT / "references_verified.csv", index=False)54 df[~df.verified].to_csv(LIT / "unverified.csv", index=False)55 log.info("verified %d / %d ; unverified: %s", df.verified.sum(), len(df), df.loc[~df.verified, "key"].tolist())56 ledger("2", "verify_refs", f"{int(df.verified.sum())}/{len(df)} verified via Crossref")5758if __name__ == "__main__":59 main()60