Skip to content

About

Claude Code skill: place pre-screened citations into LaTeX/Word manuscripts, restyle legal citations (Bluebook, OSCOLA, Chicago, APA, McGill), and add verified pinpoint page citations

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

cite-placement

A Claude Code skill that places pre-screened literature citations into a LaTeX or Word manuscript, restyles the citations already in one, or adds verified pinpoint page citations (pincites) to footnoted citations. A single Tkinter launcher selects the mode and its options.

Citation integrity — no fabricated references

This skill never invents citations. It works only with references the author has already gathered and screened:

  • Placement draws exclusively from a user-supplied, pre-screened bibliography. The inline and footnote modes place citations only from the .xlsx spreadsheet of screened references you provide. The skill decides where an existing reference belongs; it has no path that conjures a new citation from the model's own memory.
  • Every reference can be verified against authoritative databases. The optional verification pass (Phase 1.5) checks each reference against OpenAlex and CrossRef — with Google Scholar as an additional cross-check when a SEARCHAPI_API_KEY is set — and flags dangling citation keys and entries it cannot confirm.
  • Restyling reformats; it does not re-source. When converting citations from one style to another, the skill rewrites formatting only and leaves case citations, legislation, and discursive footnotes untouched, so no substantive citation content is altered or introduced.
  • Pincites are verbatim-or-nothing. The optional pincite pass writes a page number only when a verbatim quote from the acquired source text lands on a page map verified against the PDF's own printed folios. No quote match, or no verified map, means no pincite; an unpincited citation is a designed outcome, never an error, and the report says why.

Together these properties address the fabricated-citation risk that attends LLM-assisted writing: the bibliography is the author's, and the skill's role is placement and formatting rather than invention.

Overview

The skill has three modes, selected by a run button in the launcher:

  1. Inline placement — places screened citations as inline \cite{}, \citet{}, or \citep{} commands with a compiled references.bib bibliography. For economics, finance, and social-science journals using BibTeX/biber (APA, MLA, Harvard, Chicago author-date, IEEE, Vancouver). .tex only.
  2. Footnote placement — places screened citations as full formatted \footnote{} (LaTeX) or native Word footnotes (OOXML), with one-click legal and notes style selection (Bluebook, OSCOLA, Chicago notes-bib, APA 7th, McGill) and Id./supra short-form post-processing. .tex or .docx.
  3. Restyle — converts ALL existing footnote citations from one style to another, without a spreadsheet. .tex or .docx.

Footnote placement has an optional pincite pass (Phase 5.5, .tex only) that acquires each cited work's full text, verifies its printed-page map, locates verbatim supporting quotes, and rewrites the citation to carry the exact page (557 → 557, 568). The same pass runs standalone on a manuscript whose citations the author already wrote; see "Pincite Architecture" below.

Given a .tex/.docx manuscript and (for placement) an .xlsx spreadsheet of screened citations, a placement run maps the manuscript at the paragraph level, ingests the spreadsheet into BibTeX, plans where each citation belongs using parallel sub-agents, inserts the citations into a new copy of the manuscript, and compiles.

Requirements

  • Python 3.10+

  • openpyxl (pip install openpyxl) for spreadsheet ingestion.

  • For .docx manuscripts (footnote and restyle modes): python-docx, lxml, and pydantic — pip install python-docx lxml pydantic (see requirements-docx.txt). .tex-only usage needs no extra dependencies.

  • A LaTeX distribution: pdflatex + bibtex/biber for inline mode. Footnote, restyle, and pincite runs compile with the manuscript's own engine (detected by scripts/core/tex_engine.py from the author's build log or preamble); the skill never imposes an engine, because a wrong one substitutes fonts silently.

  • For the pincite pass: pymupdf and playwright (pip install -r requirements-pincite.txt, then playwright install chromium); EXA_API_KEY in the environment for book and case discovery; the Firecrawl CLI as an optional bot-wall fallback for case fetches. Scanned PDFs additionally need the pdf skill's GPU OCR runner (LightOnOCR) and easyocr.

  • For the pincite pass's scripted library lane, the institutional access variables below (User scope on Windows, so a fresh shell sees them). Without them the proxied routes fail loudly per work and everything else still runs; open-access, manual-folder and case routes need none of them.

    Variable Meaning
    CITE_PLACEMENT_EZPROXY_PREFIX Your library's EZproxy login prefix, e.g. https://<proxy host>/login?url=
    CITE_PLACEMENT_IDP_HOST The identity-provider host the proxy redirects to for the password step (ADFS)
    CITE_PLACEMENT_PROXY_AUP_BUTTON The accessible name of the button on the proxy's acceptable-use page
    CITE_PLACEMENT_LIBKEY_LIBRARY Your library's LibKey/BrowZine library id (the number in libkey.io/libraries/<id>/)
    CITE_PLACEMENT_PROXY_USER, CITE_PLACEMENT_PROXY_PASS The institutional login the scripted lane types; never committed anywhere
    CITE_PLACEMENT_CONTACT_EMAIL Polite-pool contact for Unpaywall/CrossRef (defaults to the proxy user)
    CITE_PLACEMENT_CACHE_ROOT Full-text cache and browser profile root (default ~/.claude-local/cite-placement)
  • Claude Code, invoked via /cite-placement.

  • SEARCHAPI_API_KEY in the environment to enable optional Google Scholar verification (Phase 1.5). Without it, verification falls back to OpenAlex and CrossRef only.

Directory Structure

cite-placement/
├── README.md                        # This file
├── SKILL.md                         # Router: modes, launcher protocol, commands
├── requirements-docx.txt            # Extra deps for .docx support
├── requirements-pincite.txt         # Extra deps for the pincite pass (pymupdf, playwright)
├── scripts/
│   ├── launcher.py                  # Unified Tkinter GUI (3 run buttons + utilities)
│   ├── short_form.py                # Phase 6: style-aware short-form substitution;
│   │                                #   owns the comment-aware citation split (part_spans)
│   ├── pincite_pass.py              # Phase 5.5 CLI: collect|acquire|adopt|extract|apply|repin
│   ├── merge_adjacent_footnotes.py  # Phase 5 post-proc: merge adjacent \footnote{}
│   ├── reorder_crossrefs.py         # Fix supra/Id. after manual edits
│   ├── strip_citations.py           # Remove all %CITE-PLACED footnotes
│   ├── migrate_markers.py           # Add %CITE-PLACED to legacy footnotes
│   ├── docx_restyle.py              # Standalone .docx restyle driver
│   ├── tex_restyle.py               # Standalone .tex restyle driver
│   ├── core/
│   │   ├── ingest_citations.py      # .xlsx → citations.json + references_new.bib
│   │   ├── verify_citations.py      # Phase 1.5 verification (--mode inline|footnotes)
│   │   ├── tex_engine.py            # Detect the manuscript's own TeX engine
│   │   └── docx_support/            # OOXML footnote read/write (from word-docx skill)
│   └── pincite/                     # Phase 5.5 package (README.md there = full invariants)
│       ├── stages.py                # acquire/adopt/extract/apply/repin, journaled commits
│       ├── acquire.py               # Acquisition cascade per work type
│       ├── proxy_session.py         # Authenticated EZproxy browser session
│       ├── page_map.py              # pdf page → printed page map, folio-verified
│       ├── case_pages.py            # Star-pagination / paragraph maps for judgments
│       ├── ocr_layer.py             # Invisible OCR re-layering of scanned PDFs
│       ├── quotes.py                # Verbatim quote matching
│       ├── formatter.py             # Style × work-type pincite insert/extract/strip
│       └── audit_round.py           # Parenthetical/support audit harvest + apply
├── references/
│   ├── phase-details-inline.md      # Inline-mode workflow (5 phases)
│   ├── phase-details-footnotes.md   # Footnote-mode workflow (6 phases, incl. 5.5) + Restyle
│   ├── pincite-pass-design.md       # Pincite pass: living design spec, playbooks, audit ledgers
│   ├── pincite-pass-plan.md         # Pincite pass: original implementation plan
│   ├── audit-round-PROMPT-template.md  # Prompt template for audit-round subagents
│   └── styles/                      # 5 style packs (.md + .json): bluebook, oscola,
│                                    #   chicago, apa, mcgill
├── tests/                           # pytest suite (~2,720 tests): python -m pytest tests
└── tools/
    └── cite-restyle/                # Standalone restyle app source (.exe via GitHub Release)

Quick Start (for colleagues)

This quick start walks through a restyle run — the most common colleague task. Placement runs follow the same launch flow with a different run button.

1 — Install Claude Code

npm install -g @anthropic-ai/claude-code

Run claude in your terminal to verify it works. You will need an Anthropic account or an organisation seat.

2 — Install the skill

Clone the standalone repository straight into your Claude Code skills directory (Codex users: ~/.agents/skills/cite-placement instead):

git clone https://github.com/kennethkhoocy/cite-placement ~/.claude/skills/cite-placement

or install it with its sibling skills through the plugin marketplace:

/plugin marketplace add kennethkhoocy/legal-scholarship-skills
/plugin install legal-scholarship@legal-scholarship-skills

A manual copy works too: put the cite-placement folder under %USERPROFILE%\.claude\skills\ (Windows) or ~/.claude/skills/ (macOS, Linux). The folder should contain SKILL.md, scripts/, and references/ at the top level. No further configuration is needed — Claude Code discovers skills automatically.

3 — Launch the GUI

Open a terminal, run claude, and type:

/cite-placement

Claude Code launches a Tkinter GUI window. If it doesn't appear, make sure Python's tkinter is installed (it ships with most Python distributions).

4 — Configure the restyle

In the GUI:

  • Input Manuscript — Browse and select your .docx or .tex file.
  • Output File — Auto-fills as <name>_cited.<ext>; change it if you prefer a different name or location.
  • Restyle section — Set Current Style to the style your manuscript uses (e.g., Bluebook) and Restyle To to the target style (e.g., OSCOLA).
  • Click Restyle.

The .xlsx field is for placement only; leave it empty when restyling.

5 — Claude Code runs the pipeline

After the GUI closes, Claude Code automatically:

  1. Copies your manuscript to the output path (never modifies the original).
  2. Extracts all footnotes.
  3. Reformats each footnote from the current style to the target style — Claude Code itself does the reformatting.
  4. Writes the restyled footnotes back into the output file.
  5. Reports how many footnotes were converted, skipped, and any errors.

This typically takes 1–3 minutes for a 200-footnote manuscript. A changelog is saved at <output_dir>/placement/restyle_changelog.json.

6 — Review the output

Open the output file in Word or your LaTeX editor and check that:

  • The citation format matches the target style.
  • Cross-references (e.g., (n 3) for OSCOLA) point to the correct footnotes.
  • Discursive footnotes (author commentary, data notes), case citations, and legislation were preserved unchanged.

What the Restyle Converts

✓ Journal article citations (author, title, volume, journal, page, year)
✓ Book and working-paper citations
✓ Cross-references (Id./ibid, supra note N / (n N))
✓ Introductory signals (See, See also, Cf. — added or removed per style)
✓ Internal references (Infra/Supra Part → "see Part X below/above")
✓ Author formatting (& vs "and", et al. vs "and others")
✓ Trailing punctuation (period vs no period)
✓ Asterisk/symbol footnote numbering (automatically detected)

✗ Case citations — preserved as-is (jurisdiction-specific)
✗ Legislation — preserved as-is
✗ Discursive footnotes — preserved as-is

Architecture

One launcher, one config file, three pipelines, one workspace. The launcher never runs a pipeline: it validates inputs, writes placement/run_config.json beside the output file, and exits with two status lines. Claude Code, following SKILL.md, reads the config and routes on its pipeline value; each phase's instructions live in references/phase-details-*.md, and each deterministic step is a script under scripts/.

 /cite-placement
       |
       v
 scripts/launcher.py (Tkinter)  ──writes──>  <output_parent>/placement/run_config.json
       |                                                    |
       | stdout: LAUNCHER_STATUS, LAUNCHER_CONFIG_PATH      | "pipeline": inline | footnotes | restyle
       v                                                    v
 Claude Code router (SKILL.md) ── reads references/phase-details-{inline,footnotes}.md per phase
       |
       ├─ inline ────> 1 map ─> 1.5 verify* ─> 2 ingest ─> 3 plan (3a/3b/3c) ─> 4 HITL* ─> 5 insert \cite{}
       |                                                                            + merge .bib + pdflatex/bibtex
       |
       ├─ footnotes ─> 1 map ─> 1.5 verify* ─> 2 ingest ─> 3 plan (3a/3b/3c) ─> 4 HITL* ─> 5 insert \footnote{}
       |                         + merge adjacent ─> 5.5 PINCITES* ─> 6 short forms ─> manuscript's own engine
       |
       └─ restyle ───> copy ─> extract footnotes ─> LLM restyle per section ─> write back
                        .tex: merge adjacent + short forms + own engine   |   .docx: OOXML write-back

 * optional, gated by a run_config key (verify_citations, auto_approve=false, pincites)

Who does what

Every phase is either a deterministic script or an LLM step with a fixed contract; the split is deliberate, so that nothing that can be computed is ever guessed.

Step Executor Code
1 Manuscript mapping, footnote classification, % CITE: slot enumeration Claude orchestrator (read-only) .docx footnotes read via scripts/core/docx_support/
1.5 Verification against OpenAlex, CrossRef, Google Scholar script scripts/core/verify_citations.py
2 Spreadsheet ingestion to citations.json + BibTeX script, no LLM scripts/core/ingest_citations.py
3a Assignment, 3c Consolidation Claude orchestrator plan cached on the planned_keys header
3b Per-section anchor drafting one Opus subagent per section, at most 8 concurrent style rules injected from references/styles/<id>.md
4 Human review pause only when auto_approve is false
5 Insertion orchestrator (deterministic from the cached plan) scripts/merge_adjacent_footnotes.py as the safety net
5.5 Pincites scripts around one locator fan-out scripts/pincite_pass.py, scripts/pincite/ (next section)
6 Short forms script scripts/short_form.py writes footnote_registry.json
Compile the manuscript's own engine scripts/core/tex_engine.py detects it
Restyle LLM per footnote/section, deterministic write-back scripts/tex_restyle.py, scripts/docx_restyle.py
Utilities scripts strip_citations.py, reorder_crossrefs.py, migrate_markers.py

The placement/ workspace

All three pipelines share one directory beside the output file. Phase outputs are cached there, which is what makes re-runs incremental.

<output_parent>/placement/
├── run_config.json                          launcher
├── section_map.md                           Phase 1
├── existing_citations.json | existing_footnotes.json
├── audit_report.json                        Phase 1.5 (optional)
├── citations.json, references_new.bib       Phase 2
├── assignment.json, section_plans/, placement_plan.md   Phase 3
├── footnote_registry.json                   Phase 6 (used by reorder_crossrefs.py)
├── restyle_changelog.json                   restyle mode
└── pincite/                                 Phase 5.5 (see Pincite Architecture)

Conventions every pipeline honours

  • Non-destructive. The input manuscript is copied to the output path before anything is written.
  • %CITE-PLACED marker. Every placed \footnote{ carries the marker immediately after its brace, one marker line above each citation in a merged footnote. Merge, short-form and reorder rebuilds preserve the marker multiset verbatim, and strip_citations.py removes exactly the marked footnotes.
  • One citation split. short_form.part_spans is the single definition of where one citation ends and the next begins inside a footnote: at a top-level ;, and at a comment that owns its line when the text before it ends a sentence and the text after it opens a citation or sentence. Phase 6 numbering, pincite collection and pincite application all call it, so the three never disagree about which citation a page belongs to.
  • Engine preservation. Footnote, restyle and pincite compiles use the manuscript's own engine (build log, else preamble signals). A wrong engine substitutes fonts silently, so parity with the author's PDF (font families, page count) is checked after every compile when that PDF exists.
  • Subagent cap. No fleet this skill spawns exceeds 8 concurrent agents; larger work is sharded and queued.

Phases by mode

To place new citations from an .xlsx spreadsheet, fill in the .xlsx field, select a citation style, and click Inline Placement or Footnote Placement. The pipeline then runs through all phases without pausing, except at Phase 4 when auto_approve is false.

Phase Inline mode Footnote mode
1 Manuscript mapping + package detection Manuscript mapping + footnote classification
1.5 (Optional) verify \cite{} keys vs .bib + APIs (Optional) verify footnote citations vs APIs
2 Ingest .xlsx → JSON + BibTeX Ingest .xlsx → JSON + BibTeX
3 Assign → per-section sub-agents → consolidate Assign → per-section sub-agents → consolidate
4 Human-in-the-loop review (if enabled) Human-in-the-loop review (if enabled)
5 Insert cites, merge .bib, compile (pdflatex + bibtex/biber) Insert footnotes, merge adjacent, compile with the manuscript's own engine
5.5 — (Optional) pincite pass
6 — Short-form post-processing (Id./supra), carrying pincites into short forms

See SKILL.md for the launcher protocol, the full command reference, and the run_config.json shapes; see references/phase-details-inline.md and references/phase-details-footnotes.md for per-phase detail.

Pincite Architecture (Phase 5.5)

The pincite pass is the most involved part of the skill: five deterministic stages wrapped around one LLM step, three source types with different acquisition routes and page models, a folio verifier that must refuse rather than shift, an identity model that survives manual edits, journaled multi-file writes, and three maintenance loops. About 2,590 of the skill's 2,720 tests pin it. This section is the map; scripts/pincite/README.md carries the invariants in full and references/pincite-pass-design.md the design history.

Prime directive. A pincite exists only when a verbatim quote from the acquired source text lands on a page map verified against the PDF's own printed folios. The LLM proposes a quote; code re-finds it and derives the page; the LLM never chooses a number. No quote match, or no verified map, means no pincite, and the report says why. An unpincited citation is a designed outcome, never an error.

Data flow

                       pincited-to-be .tex  +  placement/citations.json  +  run_config.json
                                            |
                                     [1] collect                      deterministic, no network
                                            |
                        works.json  +  targets.jsonl  (+ unmatched.json; re-keys quotes.jsonl)
                                            |
                                     [2] acquire                      per-work cascade, proxied routes serialized
          articles:  cache -> pdf_folder -> OA -> LibKey -> HeinOnline (EZproxy session)
          books:     cache -> pdf_folder -> OA -> Exa entitled-platform search (EZproxy)
          cases:     Exa discovery -> plain fetch of top hits -> Firecrawl on bot walls -> identity check
                     |                                        |
             cached PDF / opinion.json                 unacquired / needs-manual (URL recorded)
             + acquisition.json                               |
                     |                     operator lane: Claude-in-Chrome or pdf_folder -> [2b] adopt
                     |                                        |
                     +--------------------+-------------------+
                                          |
                                     [3] extract                      per-work pages.json
          fitz text layer -> page_map (articles, anchored) | book map (folio-inferred) | case_pages (star / paragraph)
          scanned or junk-layer PDFs: ocr_layer.py re-layers first (LightOnOCR body + easyocr folio bands)
          operator map_hints.json joins the cache signature; PAGE_MAP_VERSION bumps rebuild every map
                                          |
                                     [4] locate                       LLM fan-out, orchestrator-driven
          Opus subagents (<= 8 concurrent, one shard file each) read pages.json + targets.jsonl
          and return a verbatim body quote or null  ->  quotes.jsonl (merged key-idempotently)
                                          |
                                     [5] apply                        deterministic
          quotes.py re-finds each quote in the map -> page(s) derived -> formatter.py splices the
          pincite per (style, work type, unit) -> family reconciliation against the live tex ->
          journaled commit of .tex + pincites.json + pincite_report.md
                                          |
                                 Phase 6 short_form.py                pincites.json is authoritative;
                                                                      Id./supra carry the pages

   maintenance loops beside the flow:  repin / retire   |   audit rounds (audit_round.py)   |   post-apply triage

Stages

# Command Module(s) In → Out What it guarantees
1 pincite_pass.py collect pincite_pass.py + short_form.part_spans tex + citations.json → works.json, targets.jsonl, unmatched.json One target per citation occurrence carrying a work's full title, with its whole-footnote context and hash. Types each work (article / book / chapter / case), parses reporter cites for cases, records an explicit article number. A citation spanning an inline or braced comment is refused loudly. Quotes already located are carried across a citation that only narrowed; orphaned quotes are printed. Records screened works no footnote cites, with the reason, in unmatched.json.
2 pincite_pass.py acquire acquire.py, proxy_session.py works → cached PDF or opinion.json + acquisition.json Cascade per work type (diagram above). Every cache hit is checked against the meta.json written beside it, because the cache is global and keyed by cite key alone. Proxied routes are serialized four seconds apart. Failures record the final URL for the operator lane.
2b pincite_pass.py adopt --key K --pdf F stages.py one hand-fetched PDF → cache + acquisition row Journaled transaction (.adopt-txn/journal.json); the previous artifact is quarantined, never overwritten.
3 pincite_pass.py extract stages.py, page_map.py, case_pages.py, ocr_layer.py PDFs → <cache>/<key>/pages.json ({printed_page: text}) Builds and falsifies the printed-page map (next subsection). A map that fails verification yields no pincites for that work, with a named reason. Settles article-number candidates by evidence and writes the answer back to works.json.
4 orchestrator prompt template in references/phase-details-footnotes.md targets + pages → quotes.jsonl Locator fan-out (below). Skips an occurrence only when its stored context_hash still matches; a stale hash means relocate.
5 pincite_pass.py apply stages.py, quotes.py, formatter.py quotes → pincited tex, pincites.json, pincite_report.md Verbatim re-verification, mechanical page derivation, string surgery with an enforced inverse, loud reconciliation, one journaled commit.

Acquisition and the two browser lanes

Acquisition follows one rule set for compliance and one for provenance. The pipeline never types credentials, never purchases content, and never fetches SSRN by script (it bot-blocks); paywalled and bot-walled works are reported unacquired with the URL that stopped them.

  • Scripted lane (proxy_session.py): a persistent Chromium profile through the institutional EZproxy, headless and serialized. LibKey resolves DOIs (library id 0), HeinOnline's OneBoxCitation resolves legal cites, and the EZproxy prefix is the catch-all for publisher URLs. On an MFA prompt it stops with MfaRequired; rerun acquire --headed and approve once.
  • Operator lane: PDFs fetched in the user's real logged-in browser (Claude-in-Chrome) for MFA walls, bot-blocked hosts and SSRN, entering through adopt or the pdf_folder. The manual folder outranks every network route.
  • Discovery and fetch tiers: Exa (EXA_API_KEY) is the only discovery engine, used for books on entitled platforms and for public copies of judgments in any jurisdiction; without a key those routes report themselves skipped. Firecrawl CLI is the fallback fetch when a case candidate's plain HTTP fetch returns a bot wall or a JS shell. There are no site-specific case integrations by design.

Page maps: verified or refused

A page map is {pdf_page → printed_page}, and every builder returns the same keys so that quote matching and apply cannot tell the source types apart. Three builders exist:

  • Anchored (articles with a start page, build_page_map): the offset is known from metadata, and the job is to falsify it. Cover sheets are classified out (a page inside the work's own folio series is never a cover; boilerplate must dominate the page); the span must equal the cited range when the end page is known; every detectable header or footer folio must match the offset model, and one mismatch kills the map. Sparse anchors are normal on OCR scans.
  • Folio-inferred (books, build_book_page_map): no metadata window, so the offset comes from the folios alone; pages outside the verified envelope are null and never receive a pincite.
  • Case (case_pages): star pagination (*116) or paragraph numbers ([14]) are the map; a map says which unit it carries, and paragraph pinpoints render bracketed and trail the citation.

Two invariants bind every builder after assignment: printed pages never decrease along PDF order, and one artifact is paginated on one series. A second folio series (a reproduced booklet, an appendix restarting at 0) is refused until the operator's cover_pages or trailing_pages hint removes it; where the ranges overlap nothing in the artifact says which series is the work's. A preprint whose own folios contradict the cite is named version_mismatch, and the remedy is acquire-or-re-cite, never a hint. An article number in the page slot is settled by evidence, never by width: the anchored map is attempted first with the cited number, and only when it fails and the artifact shows no wide folios is the work mapped from its folios and rendered <number>, at <page>.

placement/pincite/map_hints.json is the operator's only channel into the map (cover_pages, end_page, drop_trailing_marker_pages, trailing_pages). Hints add evidence and relax no gate; unknown keys, unknown works, wrong types and hints on case-backed works are refused before any map is built. Hints join the cache signature (size:mtime:start:end:sha(hints)), so a map built under different hints is never reused, and PAGE_MAP_VERSION (currently 19) rebuilds every cached map on a verifier change.

OCR re-layering engages only when extract reports pages without text or a map with zero folio anchors. The pdf skill's LightOnOCR runner produces one Markdown page per true page; easyocr reads the margin bands at their true positions; ocr_layer.py injects both as invisible text, the body fenced to the middle of the page so an injected number can never read as a folio. The body directory is bound to its PDF by page coverage and a sha256 manifest, and the re-layered PDF must pass the map builder before it replaces the cached copy.

The locator fan-out

The only LLM step. The orchestrator groups targets by work, keeps only works whose map is verified, shards them into at most 8 balanced groups, and spawns one Opus subagent per shard. Each agent appends to its own quotes.shard-<i>.jsonl (exactly one writer per file); the shards merge key-idempotently into quotes.jsonl before apply.

The prompt fixes the evidence rules. The quote must be sliced verbatim from the cached pages.json text, never retyped and never taken from raw PyMuPDF output, because every later gate is a raw substring test against that file. One to three sentences; null when no passage supports the proposition. Front matter is ineligible (abstract, title block, thanks footnote, keywords, contents, headnote) and so is the introduction, which announces claims rather than developing them; conclusions are allowed. A claim stated only in front matter loses its pincite and the citation stays generic. Confidence is report metadata for the reviewer, never an insertion filter.

Authority model (protocol v2). The manuscript's main text is hand-reviewed and determinative; the explanatory parenthetical inside a placed footnote is generated and unreviewed, a prior to be updated against the article. Locators derive the proposition from the main-text context alone. Audit rounds judge support_ok against the main text and parenthetical_ok against the article, and a round that moves a quote judges the parenthetical against the replacement passage in the same row.

Apply: verbatim, mechanical, reversible, journaled

  1. Re-verify. quotes.py normalizes hyphenation, quotes, dashes and whitespace, then requires the quote as a substring of the mapped text. Not found means quote-not-found, and nothing is written.
  2. Derive. The page or page range comes from where the match lands; a quote across a page break yields a range, elided per style.
  3. Format. formatter.py splices the pincite by pure string surgery keyed on (style, work type, unit). format_pincite, extract_pincite and strip_pincite are exact inverses, enforced at the moment of writing: a result that would not read back as the pages written, or strip back to the input, is refused as format-failed. Read-back considers page groups at the citation's own parenthesis depth, so a nested "(citing ...)" citation's page is never mistaken for the outer work's. A (style, work type) pair with no rule is unsupported, not guessed.
  4. Match the live citation. The stored citation is located in the live footnote on the comment-aware split first and the ;-only split second, so a target stored before the comment-boundary rule still matches and is reported citation-commented (re-run collect), never citation-drifted.
  5. Reconcile. pincites.json must never contradict the manuscript. Reconciliation works per family (all targets sharing a footnote and a canonical citation) and keeps an entry only when live pincited copies match the family one-to-one by document order; any mismatch withdraws the whole family as pincite-withdrawn, loudly. Ambiguity refuses, never guesses.
  6. Commit. The tex, pincites.json and the report go down as one journaled commit (commit.journal.json under .commit.lock): staged, backed up, journaled, replaced. Transient PermissionErrors from Dropbox sync locks are retried; recovery rolls back; every read of the pincite directory enters through open_pincite_dir, so recovery runs first.

Every target ends the run with one action in pincite_report.md: inserted, already-pincited, no-quote, quote-not-found, unacquired, page-map-unverified, format-failed, citation-commented, citation-drifted, or pincite-withdrawn. Silent survival is a pipeline failure, the same contract as % CITE: slots.

Identity model

  • occurrence_id is cite_key#fnN, with .M for a second instance of the same work in one footnote. It is the join key between targets, quotes and pincites.
  • context_hash is sha256(citation_raw + "\0" + whole-footnote context)[:12]. It is pincite-blind (page edits do not stale it) but parenthetical-sensitive, so editing any text in a footnote stales every sibling target there; repin refreshes siblings atomically. The hash is computed over the ;-only split, so the comment-boundary rule moved no stored hash.
  • citation_raw stays canonical (un-pincited) even when the tex carries the pincited form, in every unit a style can carry. The formatter's inverses are the only page-string mechanics anywhere in the pass.
  • A comment line is a boundary, not text. The %CITE-PLACED marker and an author's own-line note bound a citation; a comment trailing live text does not, and that citation is refused rather than guessed at.

Maintenance loops

  • repin / retire is the only sanctioned mutation of targets.jsonl after a manual edit: repin --occurrence ID [--citation NEW] [--key K] recomputes the live hash, refreshes siblings, re-keys quote rows and re-binds a wrongly attributed occurrence; --retire un-places one occurrence and syncs its pincites.json entry in the same commit. Everything else treats targets.jsonl as append-only truth.

  • Audit rounds (python -m pincite.audit_round collect|apply --config CFG --round-dir DIR, prompt in references/audit-round-PROMPT-template.md) are report-only agent fan-outs whose shard output is integrity-gated (schema, newline termination, completeness against the round's manifests, live context hash, replacement quotes verbatim in the artifact) and then applied per occurrence under the same journal: retire support-false placements, splice a revised parenthetical into the citation's final top-level parenthetical group, swap quotes under the fresh hash, or withdraw a stored quote that has no body support. Every finding is revalidated against the live tex at apply time.

  • Post-apply triage buckets every not-placed occurrence and gives each bucket a standard fix, never an ad hoc edit:

    Bucket Standard fix
    wrong-version (preprint, other edition, book review) quarantine, re-acquire or adopt, re-extract, relocate
    verifier-false-negative / map-offset (repository cover sheets) map_hints.json, or a generic verifier fix with a PAGE_MAP_VERSION bump and a test
    metadata-bleed (co-cited works sharing metadata) correct works.json, repin --key
    no-folios (web-only, article-numbered) / by-design (letter folios, image-only scans) ledger the accept; a hand folio lives in the tex only

    Per-manuscript data (hint values, ledger entries) stays in the run directory; every mechanism (subcommand, hint key, verifier relaxation) lands in skill code with a test.

Artifacts

All under <output_parent>/placement/pincite/; full texts are cached outside the manuscript tree under ~/.claude-local/cite-placement/fulltext/<cite_key>/.

File Written by Contents
works.json collect (extract writes back settled article numbers) Unique works: cite key, type, title, DOI, legal cite, start/end pages
targets.jsonl collect; repin only afterwards One row per occurrence: occurrence_id, canonical citation_raw, whole-footnote context, context_hash
unmatched.json collect Screened works no footnote cites, with the reason
acquisition.json acquire, adopt Per-work outcome and provenance (route, URL, identity checks)
map_hints.json the operator, never a stage Cover, trailing, end-page hints per work
<cache>/<key>/article.pdf or opinion.json, meta.json, pages.json acquire, extract The artifact, its identity guard, and the verified printed-page text map
quotes.jsonl locator agents; re-keyed by collect and repin Verdicts joined to targets on (occurrence_id, context_hash); apply reads the first row per occurrence
pincites.json apply Authoritative occurrence-to-page map that Phase 6 reads
pincite_report.md apply One action per target, reasons, loud withdrawals
commit.journal.json, .commit.lock, .adopt-txn/ apply, adopt Journal and lock for multi-file commits; recovered on the next open

Running the pass

Tick Insert pincites in the launcher's placement section (and, optionally, name a Manual PDF folder holding <cite_key>.pdf files). It is .tex only and runs as this chain:

python scripts/pincite_pass.py collect --config placement/run_config.json
python scripts/pincite_pass.py acquire --config placement/run_config.json   # background + Monitor; --headed on MfaRequired
python scripts/pincite_pass.py extract --config placement/run_config.json
# locator subagents (at most 8 concurrent) write placement/pincite/quotes.shard-*.jsonl, merged to quotes.jsonl
python scripts/pincite_pass.py apply   --config placement/run_config.json

# operator lane and maintenance
python scripts/pincite_pass.py adopt --config placement/run_config.json --key <cite_key> --pdf <file>
python scripts/pincite_pass.py repin --config placement/run_config.json --occurrence <id> [--citation "<new>"] [--key <cite_key>] [--retire]
python -m pincite.audit_round collect --config placement/run_config.json --round-dir <dir>
python -m pincite.audit_round apply   --config placement/run_config.json --round-dir <dir>
python -m pincite.ocr_layer <in.pdf> <out.pdf> --body-dir <lightonocr per-page dir> [--drop-pages 1,2] [--allow-partial-body]

Each subcommand rejects flags that belong to another before touching any placement file. After upgrading the skill, re-run collect before apply and read its own lines (re-keyed, refused, orphaned quotes).

Standalone mode

The pass has no dependency on the placement pipeline. Every subcommand reads only three config keys (output_tex, citation_style, pdf_folder), placement/citations.json, and the manuscript; never %CITE-PLACED markers, a .bib, or any other placement artifact. A manuscript whose citations the author already wrote runs the same chain once citations.json is harvested from its footnotes (recipe: "Standalone pincite mode" in SKILL.md). Existing short forms are out of scope by construction, because collect targets only occurrences carrying a work's full title.

Further reading

  • scripts/pincite/README.md: the package's own architecture document, with every invariant and the audit ledger summary.
  • references/pincite-pass-design.md: the living design spec, acquisition constants, remediation playbooks, and every hardening round with dates.
  • references/phase-details-footnotes.md, Phase 5.5: the locator protocol and prompt template.

Standalone Scripts

Deterministic ingestion (no LLM) of an .xlsx spreadsheet into BibTeX:

python scripts/core/ingest_citations.py \
    --input citations.xlsx \
    --output-json citations.json \
    --output-bib references_new.bib \
    --existing-bib references.bib

Optional: --min-score N skips spreadsheet rows whose screening_score is below N (default 0 = no filtering).

Verify existing citations against OpenAlex, CrossRef, and optionally Google Scholar (mode selects the input shape and report schema):

# inline: verify \cite{} keys against the .bib
python scripts/core/verify_citations.py --mode inline \
    --citations placement/existing_citations.json \
    --bib references.bib \
    --output placement/audit_report.json \
    --spreadsheet citations.xlsx

# footnotes: verify extracted footnote citations
python scripts/core/verify_citations.py --mode footnotes \
    --footnotes placement/existing_footnotes.json \
    --output placement/audit_report.json \
    --spreadsheet citations.xlsx

Set SEARCHAPI_API_KEY in your environment to enable Google Scholar verification.

Standalone .exe (tools/cite-restyle)

For colleagues without a Python or Claude Code setup, the restyle workflow is also packaged as a standalone Windows executable built from the source in tools/cite-restyle/ using its bundled PyInstaller .spec. The .exe is distributed only as a GitHub Release asset and is never committed to the repository (the built binary is large and platform-specific). A short README-dist.txt ships alongside the .exe in the release, explaining how to run it and which styles it supports. To rebuild, run PyInstaller against the .spec in tools/cite-restyle/ and upload the resulting .exe plus README-dist.txt to a new release.

Troubleshooting

  • The GUI doesn't open. Confirm tkinter is installed: python -c "import tkinter". On Ubuntu/Debian: sudo apt install python3-tk.
  • "Permission denied" on the output file. The output file may be open in Word, or it lives in a Dropbox- or OneDrive-synced folder that has locked it. Close Word and/or write to a local directory.
  • Claude Code doesn't recognise /cite-placement. Check the skill is in the correct directory with SKILL.md at the top level, then restart Claude Code.
  • Some footnotes weren't converted. Discursive footnotes, case citations, and legislation are intentionally skipped — check the changelog for details.
  • Undefined citation warnings (inline mode). Check .bib key spelling and that \bibliography{} / \printbibliography is present; a missing .bst file means the relevant LaTeX package needs installing.
  • A citation was refused as "spans a LaTeX comment" (pincite pass). A % comment trails live text inside the citation, or sits inside a brace group. Move the comment onto its own line (or delete it) and re-run collect.
  • Works reported unacquired (pincite pass). Expected for paywalled books, bot-walled hosts, and SSRN. Drop <cite_key>.pdf into the manual PDF folder or run adopt, then re-run extract.
  • The compiled PDF shows different fonts or a different page count. The manuscript was compiled with the wrong engine. Run python scripts/core/tex_engine.py main.tex and compile with what it reports.

Key Design Decisions

  • Non-destructive: never modifies the original manuscript — always creates a new copy.
  • Re-run safe: .bib merging avoids duplicates; placement plans are incremental, keyed on the planned_keys header.
  • Agentic Phase 3: parallel sub-agents (one per section) prevent citation drops on large batches.
  • Content-agnostic: works for any academic discipline — no hardcoded section names.
  • Style-swappable: footnote citation styles change via the GUI without re-running placement decisions; the restyle mode converts existing citations between styles.
  • Verbatim-or-nothing pincites: a page is written only from a quote re-verified against a folio-checked page map; every refusal is reported with its reason, and multi-file writes are journaled so a failed run rolls back.
  • Engine preservation: footnote, restyle, and pincite compiles use the manuscript's own TeX engine and verify font and page-count parity against the author's PDF when one exists.

Lineage

This skill consolidates the former cite-placement-inline and cite-placement-footnotes skills into a single router with a shared launcher and a common placement/ workspace.

About

Claude Code skill: place pre-screened citations into LaTeX/Word manuscripts, restyle legal citations (Bluebook, OSCOLA, Chicago, APA, McGill), and add verified pinpoint page citations

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages