"""Verify reference candidates against Crossref (works API) and write research/prior_art.csv. For each candidate: query.bibliographic = " ", rows=4; a match requires (i) the first-author family name appears among the returned authors, (ii) |year - candidate year| <= 1, (iii) normalized title similarity >= 0.80 (difflib). Books/chapters without a Crossref DOI are reported as UNVERIFIED_CROSSREF and must be verified by other means (publisher/WorldCat) and flagged in the paper. """ from __future__ import annotations import csv, difflib, json, re, sys, time, unicodedata from pathlib import Path import requests ROOT = Path(__file__).resolve().parents[3] CAND = ROOT / "research" / "reference_candidates.csv" OUT = ROOT / "research" / "prior_art.csv" RAWJ = ROOT / "research" / "crossref_raw.json" UA = "WP35-UQO-bib-verify/0.1 (mailto:simon-pierre.boucher@uqo.ca)" def norm(s: str) -> str: s = unicodedata.normalize("NFKD", s or "").encode("ascii", "ignore").decode() return re.sub(r"[^a-z0-9 ]+", " ", s.lower()).strip() def sim(a: str, b: str) -> float: return difflib.SequenceMatcher(None, norm(a), norm(b)).ratio() def crossref(q: str): r = requests.get("https://api.crossref.org/works", params={"query.bibliographic": q, "rows": 4}, headers={"User-Agent": UA}, timeout=60) r.raise_for_status() return r.json()["message"]["items"] def year_of(it): for k in ("published-print", "published-online", "issued", "created"): d = it.get(k, {}).get("date-parts", [[None]])[0][0] if d: return d return None def main(): rows = [r for r in csv.DictReader(open(CAND), delimiter="|") if r["type"] != "skip"] results, raw = [], {} for r in rows: q = f'{r["first_author"]} {r["year"]} {r["title"]}' try: items = crossref(q) except Exception as e: results.append({**r, "status": f"CROSSREF_ERROR {e}", "doi": "", "cr_title": "", "cr_year": "", "cr_container": "", "title_sim": ""}) continue raw[r["key"]] = items best = None for it in items: t = (it.get("title") or [""])[0] fam = [norm(a.get("family", "")) for a in it.get("author", [])] + [norm(a.get("name", "")) for a in it.get("author", []) if "name" in a] if "editor" in it: fam += [norm(a.get("family", "")) for a in it["editor"]] y = year_of(it) s = sim(r["title"], t) if r["type"] in ("book",) and it.get("type") in ("book", "monograph", "edited-book", "reference-book") and s < 0.8: s = max(s, sim(r["title"].split(":")[0], t.split(":")[0])) ok_auth = any(norm(r["first_author"]) in f for f in fam) if fam else False ok_year = y is not None and abs(int(y) - int(r["year"])) <= 1 score = s + 0.1 * ok_auth + 0.1 * ok_year if best is None or score > best[0]: best = (score, it, t, y, s, ok_auth, ok_year) if best and best[4] >= 0.80 and best[5] and best[6]: it = best[1] status = "VERIFIED_CROSSREF" elif best and best[4] >= 0.80 and best[6] and not best[5]: it = best[1]; status = "TITLE_YEAR_MATCH_AUTHOR_MISSING" else: it = best[1] if best else {} status = "UNVERIFIED_CROSSREF" results.append({**r, "status": status, "doi": it.get("DOI", ""), "cr_title": (it.get("title") or [""])[0], "cr_year": year_of(it) if it else "", "cr_container": (it.get("container-title") or [""])[0] if it else "", "title_sim": round(best[4], 3) if best else ""}) print(f'{r["key"]:28s} {status:32s} sim={results[-1]["title_sim"]} doi={it.get("DOI","")}') time.sleep(0.25) with open(OUT, "w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(results[0].keys()), delimiter="|") w.writeheader(); w.writerows(results) RAWJ.write_text(json.dumps(raw, indent=1)) n_ok = sum(r["status"] == "VERIFIED_CROSSREF" for r in results) print(f"\n{n_ok}/{len(results)} VERIFIED_CROSSREF -> {OUT}") if __name__ == "__main__": main()