#!/usr/bin/env python3 """ Archive a finished literature-search-verify session into a permanent, project-level folder instead of leaving results sitting in the skill's own scratch output/ directory (which is easy to lose track of across sessions and isn't meant to be a durable deliverable location). Bundles the verified BibTeX file -- and, if given, any downloaded PDFs -- into /references//, and writes a README.md index (entry list, suspect/unverified entries flagged separately, free- text coverage notes) so a future session or a human can find and trust what's there without re-reading the conversation that produced it. No third-party dependencies; uses only the standard library. CLI usage (run from the project root; scratch input lives under output/literature-search-verify//, matching the slug this script derives from the topic argument): python3 .claude/skills/literature-search-verify/scripts/archive_references.py \\ "UAV aeromagnetic compensation" \\ --bib output/literature-search-verify/uav_aeromagnetic_compensation/uav_aeromagnetic_compensation_final.bib \\ --project-root . \\ --pdfs-dir output/literature-search-verify/uav_aeromagnetic_compensation/pdfs \\ --suspect "Some fabricated-looking title|DOI resolves but venue is topically unrelated" \\ --notes "Kalman-filter and GA/PSO angles searched, no on-topic hits found." Output: prints the path of the archive directory that was created/updated. """ import argparse import os import re import shutil import sys from datetime import date def slugify(text): """ASCII gets lowercased; CJK characters are kept as-is (there's no reversible case-fold for them) so an all-Chinese topic name doesn't collapse to nothing and fall back to the generic "references" name, colliding with every other Chinese-named topic archived in this repo.""" text = text.strip().lower() text = re.sub(r"[^a-z0-9一-鿿]+", "_", text) return text.strip("_")[:60] or "references" def parse_bib_entries(bib_path): """Minimal BibTeX parser -- just enough to pull key/title/year/venue/doi/note (plus the raw entry text, for reordering) for the README index. Not a general-purpose BibTeX parser.""" with open(bib_path, encoding="utf-8") as f: content = f.read() entries = [] for m in re.finditer(r"@(\w+)\{([^,\n]+),(.*?)\n\}", content, re.S): entry_type, key, body = m.groups() fields = {} for fm in re.finditer(r"(\w+)\s*=\s*\{(.*?)\}\s*,?\s*(?=\n\s*\w+\s*=|\n\Z|\Z)", body, re.S): fields[fm.group(1).lower()] = re.sub(r"\s+", " ", fm.group(2)).strip() entries.append({"type": entry_type, "key": key.strip(), "raw": m.group(0).strip(), **fields}) return entries def year_sort_key(entry): """Chronological order, oldest first; entries with no parseable year sort last.""" year_str = re.sub(r"[^0-9]", "", entry.get("year", "") or "") year = int(year_str) if year_str else 9999 return (year, entry.get("key", "")) def build_readme(topic, entries, pdf_count, suspect, notes): lines = [] lines.append(f"# {topic} — literature archive") lines.append("") lines.append(f"Archived: {date.today().isoformat()}") lines.append(f"Verified entries: {len(entries)}") lines.append(f"PDFs bundled: {pdf_count}") lines.append("") lines.append( "Every entry in `references.bib` passed independent verification " "(arXiv ID / DOI resolution and/or cross-source title match, " "similarity >= 0.9) via the literature-search-verify skill before " "being archived here. Citation keys follow the surname+year " "convention and are stable -- the paper-writing-grounded skill's " "`\\cite{}` calls should match these keys directly." ) lines.append("") lines.append("## Entries (chronological, oldest first)") lines.append("") for e in entries: title = e.get("title", "?") year = e.get("year", "?") venue = e.get("journal") or e.get("booktitle") or e.get("school") or "" doi = e.get("doi", "") note = e.get("note", "") line = f"- **{e['key']}** ({year}) — {title}" if venue: line += f". *{venue}*" if doi: line += f". DOI: {doi}" lines.append(line) if note: lines.append(f" - Note: {note}") if suspect: lines.append("") lines.append("## Flagged during search — NOT included above, do not cite") lines.append("") for s in suspect: parts = s.split("|", 1) title = parts[0].strip() reason = parts[1].strip() if len(parts) > 1 else "" lines.append(f"- {title}" + (f" — {reason}" if reason else "")) if notes: lines.append("") lines.append("## Search coverage notes") lines.append("") lines.append(notes) return "\n".join(lines) + "\n" def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("topic", help="Human-readable topic name, e.g. \"UAV aeromagnetic compensation\"") ap.add_argument("--bib", required=True, help="path to the curated/verified .bib file to archive") ap.add_argument("--project-root", default=".", help="project root; archive is written under /references//") ap.add_argument("--pdfs-dir", default=None, help="optional folder of open-access PDFs to copy alongside the bib") ap.add_argument("--suspect", action="append", default=[], help="title|reason of a suspect/unverified entry to log; repeatable") ap.add_argument("--notes", default=None, help="free-text notes on search coverage/gaps for the README") args = ap.parse_args() if not os.path.isfile(args.bib): print(f"error: bib file not found: {args.bib}", file=sys.stderr) sys.exit(1) slug = slugify(args.topic) archive_dir = os.path.join(args.project_root, "references", slug) os.makedirs(archive_dir, exist_ok=True) bib_dest = os.path.join(archive_dir, "references.bib") shutil.copyfile(args.bib, bib_dest) entries = parse_bib_entries(bib_dest) entries.sort(key=year_sort_key) # Rewrite the archived .bib in chronological order (oldest first) so the # file itself, not just the README, reads as a timeline. header = f"% {args.topic} -- verified references, chronological order\n% Archived {date.today().isoformat()}\n\n" with open(bib_dest, "w", encoding="utf-8") as f: f.write(header) f.write("\n\n".join(e["raw"] for e in entries)) f.write("\n") pdf_count = 0 if args.pdfs_dir and os.path.isdir(args.pdfs_dir): pdf_dest_dir = os.path.join(archive_dir, "pdfs") os.makedirs(pdf_dest_dir, exist_ok=True) for fn in sorted(os.listdir(args.pdfs_dir)): if fn.lower().endswith(".pdf"): shutil.copyfile(os.path.join(args.pdfs_dir, fn), os.path.join(pdf_dest_dir, fn)) pdf_count += 1 readme = build_readme(args.topic, entries, pdf_count, args.suspect, args.notes) with open(os.path.join(archive_dir, "README.md"), "w", encoding="utf-8") as f: f.write(readme) print(archive_dir) if __name__ == "__main__": main()