skills/bluebook-audit/skills/audit-crossrefs/SKILL.md
Phase 7: Convert hardcoded cross-references to auto-updating NOTEREF fields
npx skillsauth add edwinhu/workflows audit-crossrefsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Convert hardcoded supra/infra note numbers to NOTEREF field codes that auto-update when footnotes are renumbered, then tie each cross-reference to a bibkey from references/sources.bib so the bibliography becomes the semantic identity layer.
sources.bib ← make_bib_from_docx.py # BOOTSTRAP (once per paper, Gemini)
↓
docx ← create_crossrefs.py # ONE-TIME conversion of hand-typed text
↓
docx ← audit_crossref_targets.py --grep --apply # DRIFT CORRECTION (deterministic)
↓
docx ← bib_integrate.py --bib references/sources.bib # BIBKEY-TAG SYNC (deterministic)
(also pre-computes cached display values)
All four live in ${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/.
After a Word session that added/edited footnotes (including new hand-typed
<X>, supra note N text), run:
"${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/sync_crossrefs.sh" \
draft.docx references/sources.bib
The shell wrapper runs create_crossrefs → audit_crossref_targets → bib_integrate in sequence. All three are idempotent and fully deterministic — re-running on a clean doc does nothing; ~5 seconds total runtime.
| Stage | Script | Gemini? | When |
|-------|--------|---------|------|
| Bootstrap | make_bib_from_docx.py | Yes (one-off batch) | Paper has hand-typed footnotes and no sources.bib yet |
| Initial conversion | create_crossrefs.py | No | Convert hardcoded supra note N text → NOTEREF fields |
| Drift correction | audit_crossref_targets.py --grep | No | After footnote edits — fix any supras whose target drifted |
| Drift correction (ambiguous) | audit_crossref_targets.py --batch | Yes | Only for refs grep can't resolve uniquely |
| Bibkey-tag sync | bib_integrate.py | No | After cross-refs settle — rename bookmarks to _RefBib_<bibkey> and pre-compute cached display |
Convert hardcoded <X>, supra note N text to NOTEREF cross-reference fields.
# Preview
uv run python3 "${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/create_crossrefs.py" --docx <path> --dry-run
# Apply
uv run python3 "${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/create_crossrefs.py" --docx <path>
references/sources.bib yet):
uv run --with lxml --with google-genai --with google-cloud-storage python3 \
"${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/make_bib_from_docx.py" \
--docx <path> --out references/sources.bib
Walks the docx footnotes, skips supra-only / bio fns, sends each first-cite candidate to Gemini Vertex Batch, emits BibTeX with note = {fnN} linking each entry to its source footnote.audit_crossref_targets.py --grep --apply (see below). Hand-typed supra note N numbers go stale as footnotes are added or reordered; the create_crossrefs script faithfully bookmarks the literal N, which can be silently wrong for every reference. Grep resolves the easy cases deterministically; only ambiguous refs need --batch.bib_integrate.py --bib references/sources.bib --apply. Renames bookmarks to _RefBib_<bibkey>, retargets supras by bibkey, and pre-computes cached display values so the doc renders correctly on first read (no F9 needed).fn+F9 (Mac) or Ctrl+A → F9 (Windows) to force-refresh fields if needed.audit_crossref_targets.py (drift correction, also runnable standalone)# Mechanical audit only — flags refs whose surnames don't appear in the target footnote
uv run --with lxml python3 \
"${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/audit_crossref_targets.py" \
--docx <path>
# Deterministic first pass — grep each reference for its first cite. No LLM cost.
# On a 138-ref test set: resolved 62 refs unique with 98% accuracy, deferred
# 76 truly ambiguous cases. Always run this BEFORE invoking Gemini/Batch — it
# strips the easy cases and saves LLM calls.
uv run --with lxml python3 \
"${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/audit_crossref_targets.py" \
--docx <path> --grep --apply
# Grep + production LLM batch for the residue (recommended for full pipeline)
uv run --with lxml --with google-genai --with google-cloud-storage python3 \
"${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/audit_crossref_targets.py" \
--docx <path> --grep --batch --apply --location global
# LLM-only paths (skip grep — coverage-risky for --gemini):
uv run … --docx <path> --batch --model gemini-3.1-flash-lite-preview --apply
uv run … --docx <path> --gemini --apply
bib_integrate.py (bibkey-tag sync, fully deterministic)uv run --with lxml python3 \
"${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/bib_integrate.py" \
--docx <path> --bib references/sources.bib --apply
What it does:
sources.bib.fn_id → bibkey map from current docx content — sidesteps any drift between bib's note={fnN} tag and current numbering._RefBib_<bibkey> bookmarks wrapping body footnoteReferences. Multi-cite footnotes get multiple bookmarks (one per source) — each pointing at the same body footnoteReference, so the displayed number stays the same; only the bookmark name differs by source.--bio-count, default 3 for *, †, ‡ author bios that don't consume display numbers when numRestart=eachSect is set).SUPRA_BIB_AUDIT.md listing any supras whose surnames don't match the picked bibkey's haystack (author + title + howpublished + bibkey).Fully deterministic — no LLM calls. Idempotent on re-runs.
make_bib_from_docx.py (bootstrap, Gemini)uv run --with lxml --with google-genai --with google-cloud-storage python3 \
"${CLAUDE_SKILL_DIR}/../../../../skills/bluebook-audit/scripts/make_bib_from_docx.py" \
--docx <path> --out references/sources.bib
Walks the docx footnotes, splits multi-cite footnotes on ;, sends each first-cite candidate to a Vertex AI Batch job (one independent request per citation; gemini-3.1-flash-lite-preview default). Emits BibTeX entries with bibkey conventions firstauthorlastYEAR for academic works and short slugs (gao2017, crs2024, secReg2020) for institutional sources.
Each entry includes note = {fnN} linking back to the source footnote.
Once sources.bib exists, maintain it directly — do not regenerate. Edits to the bib (typos, missing fields, new sources) should be in-place.
Vertex AI batch requires:
gcloud auth application-default login for ADC--project (default $GOOGLE_CLOUD_PROJECT or activist-defense-nal)--gcs-bucket (default $GEMINI_BATCH_BUCKET or nal-batch-extraction)us-central1 (default)For gemini-3.x models, thinkingLevel: MINIMAL is set automatically — without it, batch responses silently return empty content (see /gemini-batch SKILL gotcha 12).
Output written to <docx-dir>/scratch/:
crossref_audit.json — every cross-reference with current target + match statuscrossref_remap.json — Gemini's proposed corrections (with confidence)CROSSREF_AUDIT.md — human-readable diff reportThe audit is safe to run standalone without re-running the rest of the bluebook-audit workflow. Use it any time create_crossrefs has been run and you want to validate that hand-typed supra/infra numbers were correct.
--grep first. Cheapest, deterministic, ~98% precision on unique
matches. Resolves ~half the refs at $0 cost.--batch (Vertex AI, per-ref independence) on the deferred set.--grep worksA cross-reference reads <short-form identifier>, supra/infra note N. The
identifier can be any Bluebook short form, not just author surnames:
Hu, Malenko & ZytnickGAO Report, CRS Report, Rosenberg, 2020 SEC RegulationBest Practice Principles, Policies and Procedures, Senate Banking Committee LetterISS v. SECChoi, Fisch & Kahan, Power of Proxy AdvisorsFor each reference the script:
See, But see, E.g.).<X>, supra/infra note <N> phrase from each candidate footnote, so a footnote that merely cites the same identifier via supra doesn't count as the first cite.unique, apply deterministically.ambiguous, defer to LLM (picking the earliest is wrong ~17% of the time — typically when the identifier appears in passing in one footnote and as a first cite in another).no_match, defer to LLM (which has the catalog and can guess, or correctly say "not in document").Iron Law: Neither Gemini pass is fully trustworthy on its own. A two-batch consensus is high-confidence, but the residual disagreements ALWAYS require human first-cite verification. Do not stop after --apply.
The failure modes we observed on a real 248-footnote draft:
See also or multi-cite list (model misses it and picks a different paper by the same author)See/hereinafter/inline-discussion lead-ins are flagged as mismatches even when correct.for fn in fns.findall('.//w:footnote'):
txt = ''.join(t.text or '' for t in fn.iter('w:t'))
if re.search(r'<surname-pattern>', txt) and 'supra' not in txt[:300].lower():
print(fn.get('w:id'), txt[:200])
(source_fn, surnames, current_bookmark) → correct_fn_id, save as crossref_remap_manual.json, and apply with --remap PATH --apply.See also/multi-cite list.SUPRA_UNRESOLVED.md listing the source FN, surnames, and the search patterns tried.--gemini and --batch)SUPRA_UNRESOLVED.md written (or empty if all resolved)| Pattern | Example | Result |
|---------|---------|--------|
| Single supra | supra note 42 | NOTEREF to FN42 bookmark |
| Single infra | infra note 188 | NOTEREF to FN188 bookmark |
| Range | infra notes 209-210 | Two NOTEREFs with separator |
| With pincite | supra note 42, at 15 | NOTEREF + roman , at 15 |
| Existing NOTEREF | (already converted) | Skipped |
supra Section I.A., infra Part III)audit_crossref_targets.py run + Step 4 manual verification complete; no NOTEREF points at an unrelated footnotePresent final summary to user:
development
Build the meeting-level proxy-voting × ownership panel on the WRDS SGE grid — ISS N-PX fund votes reduced to (item × block) direction cells, joined to institutional and mutual-fund ownership. Use when working with risk.voteanalysis_npx, N-PX fund-level votes, ISS→CRSP fund linking, index/passive/active voting blocks, or a proxy-voting panel that needs ownership attached.
development
Use when "CRSP CIZ", "CRSP v2", "CRSP flat file format 2.0", "crsp.dsf_v2 / msf_v2", "StkDlySecurityData", "StkMthSecurityData", "StkSecurityInfoHist", "stocknames_v2", "DlyRet / MthRet / DlyPrc / MthPrc", "SHRCD or EXCHCD equivalent in new CRSP", "SIZ to CIZ migration", "CRSP data after 2024", "CRSP delisting returns", "CRSP cumulative adjustment factors", "CRSP index INDNO / INDFAM", or any CRSP stock/index query where the legacy SIZ column names no longer exist.
development
Use when linking or deduping datasets by entity name rather than a shared key — 'fuzzy match', 'fuzzy name matching', 'entity resolution', 'record linkage', 'match company/person names', 'dedupe entity names', 'name-based join', 'bridge identifiers' (CIK ↔ permno ↔ gvkey ↔ wficn ↔ EIN ↔ personid), or any use of char n-gram TF-IDF, cosine similarity on names, `sparse_dot_topn`, or RapidFuzz at scale.
development
Use when building a publication-quality table in Python — 'regression table', 'results table', 'summary statistics table', 'etable', 'coefplot', 'great_tables', 'GT', 'gt table', 'format a table for the paper', 'export table to LaTeX/HTML', significance stars, spanners, or column formatting for a table headed into a paper, slide deck, or notebook.