11# ts-paper-cite — real, complete citations (no fabrication)
12
13Citations support real claims with real evidence. **Every entry in refs.bib is a paper that
14actually exists and that you verified.** This stage replaces the original 100+-call retrieve→triage→
15slot→bind→postdraft engine (which fabricated title-only stubs to hit hardcoded quotas) with a few
16Claude reasoning passes + native web search.
17
18## Procedure — broad systematic retrieval, then triage (adopt the original engine's *coverage*, keep integrity)
19A TS journal paper is expected to be **well-read**: it situates itself in a full literature, not a minimal one. Do NOT stop at "just enough" — search **broadly and systematically**, then keep only the real, well-matched papers. This is the original product's strength (systematic query planning → retrieve many → triage), minus its fabrication.
20
210. **Reuse the idea2story seed first (if present).** If retrieved_papers.json exists in the workdir (written by **ts-idea2story**, which already searched the literature), ingest it as a **verified seed**: for each paper run scripts/doi2bib.py <doi> to upgrade it to complete BibTeX (the DOI must resolve, or drop it — Crossref is authoritative over the seed's own metadata), and use its abstract+supports_claim to place it into claims_map.json. This is NOT a free pass — it removes the cold-start sweep, but you still verify each, place by abstract, and gap-fill below. Then the angles you must still search are the ones the seed does NOT cover.
221. **Plan the search angles (query packs).** From the proposal + blueprint (and what the seed already covers), enumerate the angles a thorough reviewer expects covered, and write them to **retrieval_plan.md**:
23 - the **core task** and its canonical benchmark/problem papers;
24 - **each method component / technique** the paper builds on (every named module, predictor, feature, loss);
25 - **every dataset and every metric** named in the experiment design;
26 - **every baseline** named;
27 - the **adjacent lineages** a reviewer would expect (neighbouring tasks, the families your method departs from);
28 - recent **surveys / foundational** works of the area.
292. **Retrieve broadly.** Run a WebSearch/WebFetch per angle (this is many searches, not "a handful" — breadth is the point). Collect candidate papers per angle. Prefer authoritative venues; capture the DOI/arXiv id as you go.
303. **Read each candidate's ABSTRACT to triage AND to place it.** Fetch the abstract (WebFetch the venue/arXiv/Crossref page, or read the search snippet/TLDR) and use it to decide two things at once — this replaces the original engine's abstract → family classification → section routing:
31 - **triage**: same-task / same-technical-lineage (keep) vs cross-domain / loose-overlap (reject). Surfacing many and filtering is better than finding the minimum.
32 - **placement**: which section the paper belongs in and which exact claim it supports — record this as the section + claim in claims_map.json. Do NOT slot a paper by its title alone; the abstract is what justifies both "is it relevant" and "where does it go".
33 Log kept/rejected counts per angle in retrieval_plan.md.
344. **Fetch complete metadata** for every kept paper via scripts/doi2bib.py <doi> (deterministic, ASCII-folded, no LLM). User-provided references take priority and are preserved exactly.
355. **No real paper supports a claim → don't cite it.** Soften/rephrase or drop the claim. A real paper found by broader search is the only way to raise the count — **never** a stub, never an off-topic filler.
366. **Write refs.bib** (complete entries only) + claims_map.json, then run the linter (which now enforces a coverage floor).
37
38## Every entry must be complete and real
39- Required fields: **authors, year, venue (journal/booktitle), volume(issue), page range, and DOI** (DOI or stable URL; arXiv id ok for preprints). **Title alone is not enough.**
40- Use @article/@inproceedings/@book with all fields populated. **A bare @misc{key, title={...}} stub is forbidden.** If you can only find a title, the paper is not verified — do not cite it.
41- Verify existence yourself: build/verify each entry through scripts/doi2bib.py <doi> (which actually resolves the DOI against Crossref) or a real venue/publisher/arXiv/Crossref/DBLP page. citations_lint.py by default only checks **structural completeness** (the fields are present) — it cannot tell a real DOI from an invented one; DOI resolution is enforced only under the opt-in citations_lint.py <workdir> --resolve flag (and by doi2bib.py), not by the default lint. Do not trust a title that only appears in your own generated text.
42- One bibkey per real paper (firstauthorYEARkeyword, ASCII, no leading digit); reuse it everywhere; never duplicate a paper under two keys.
43- **Fast path to complete metadata:** once you have a DOI, run scripts/doi2bib.py <doi> to fetch a fully-populated BibTeX entry from Crossref deterministically (no hand-transcription). For an **arXiv-only preprint**, give doi2bib.py the arXiv id instead — it takes an arXiv path (arXiv API → eprint/archivePrefix, journal={arXiv preprint}) so the venue field is satisfied without a Crossref DOI.
44- **Existence of arXiv-only entries:** citations_lint.py --resolve resolves DOIs only; it **skips** entries that have an eprint (arXiv) and no doi, treating the arXiv id as its own existence signal — so a legitimate preprint is never wrongly flagged doi_unresolved.
45
46## How many — coverage target met with REAL papers (not a fabrication quota), per the TEMPLATE
47Coverage is **template-driven and enforced on TWO axes** by citations_lint.py — a global count
48**and** a per-section distribution. The global floor alone is NOT sufficient: a paper can clear 40
49total cites while whole body sections carry zero and related_work overflows its band. Both axes read
50template.json's citations block:
51- **Global floor** (citations.floor; TS ts_iieta = **40**, target ~40–50; neurips = **30**) →
52 coverage_below_floor if under.
53- **Per-section bands** (citations.per_section_coverage, e.g. related_work:[20,30],
54 introduction:[8,12]) → an evidence-bearing body section (introduction/related_work/method/
55 experiments/analysis) with **zero** cites is a **HARD error** section_zero_citations; a non-empty
56 section **outside its band** is a **warning** section_coverage_out_of_band (rebalance — move/merge
57 real cites; never fabricate or delete real support). Citation-free sections in a proposal
58 (abstract, conclusion, future_work) are exempt from the zero-error.
59
60A well-read paper sits at/above the floor **and** spreads its evidence across every body section. The
61**only** way to satisfy both is to *search more broadly for real papers and place each in the section
62whose claim it actually supports* — never to invent, pad, or dump everything into related_work.
63Stub/incomplete entries hard-fail separately, so coverage can only be cleared with real, fully-specified
64papers ("search harder", never "fabricate"). The seed from ts-idea2story (step 0) counts toward this
65once each entry is verified.
66
67Per-section coverage (real papers; expand the search until each is genuinely covered):
68- **related_work** — the heaviest: map the *full lineage* of every theme (typically **20–30** distinct works), so a reviewer sees you command the field.
69- **introduction** — solid background + every prior-method attack cited (**8–12**).
70- **method** — cite **every** technique/module/predictor/feature/loss you build on or contrast (one per real predecessor).
71- **experiments** — cite **every** dataset, **every** metric, and **every** baseline named.
72- abstract/conclusion — none.
73
74To reach 40–50 honestly, widen the angle set (more sub-lineages, more surveys, the foundational works behind each component, neighbouring tasks) rather than padding with off-topic papers. If after a genuinely broad search a niche topic truly has little prior work, fewer is acceptable and honest — but for a mainstream TS topic, under 40 almost always means the search was too shallow. Do **not** bloat with off-topic papers to hit the number; an off-topic cite fails claims_map.json's weak_claim_match anyway.
75
76## IIETA in-text style (used by the write stage)
77Numeric [n] via natbib: the bundled ts_iieta.sty sets \setcitestyle{numbers,square,comma,sort&compress} with \bibliographystyle{unsrtnat}. The write stage emits \cite{bibkey} (or \cite{k1,k2,k3} for grouped) — never hand-typed [1]. No citations in the abstract or in headings.
78
79## Honesty (proposal — no real results)
80Cite only to support claims about prior work / datasets / methods / metrics. **Never** cite to imply your proposed method has results it doesn't, and never hang a citation on a fabricated "outperforms by X%" sentence.
81
82## Emit claims_map.json (enforced claim↔citation matching)
83Alongside refs.bib, write **claims_map.json** mapping every bibkey you will cite to its justification:
84```json
85{ "chung2016syncnet": {"claim": "lip-sync models detect audio-video offset by contrasting windows",
86 "support_label": "same_line_support", "section": "related_work"} }
87```
88support_label ∈ {direct_core, same_line_support, context, baseline, dataset_metric, definition}. If the honest label for a match is off_topic/adjacent/weak, **do not cite** — drop or rephrase the claim. This makes the one-pass claim-matching an auditable artifact instead of a trust-me step.
89
90## Post-draft saturation sweep (REQUIRED after write/refine — it is now gated)
91Citations are inserted in the write stage, so the per-section bands above are only meaningful **after
92the draft exists**. Re-running citations_lint.py on the drafted sections/*.tex is mandatory: it now
93fails on any evidence-bearing section left with zero cites (section_zero_citations) and on any
94claims_map section that disagrees with where the key is actually cited
95(claims_map_section_mismatch). Do a **positive** pass to catch citeable sentences the draft introduced
96that carry no cite — without porting the product's heavyweight ledger/cluster engine (the per-section
97gate distills its essential purpose deterministically).
981. **Find under-cited sections.** Scan each finalized section; flag any *factual claim about prior work /
99 datasets / baselines / metrics* with no \cite. (Self-method, self-results, and future-work sentences
100 are non-citeable — never bolt a prior-work cite onto a results sentence.)
1012. **Fill with real papers only**, via the same WebSearch + doi2bib.py + claims_map.json discipline;
102 honesty-by-omission if no real paper fits. At most 3–4 new cites per rewritten paragraph; preserve
103 every existing \cite exactly.
1043. **Two cheap precision devices (Claude-native, no code):** (a) **name the nearest wrong neighbor** — for
105 each citeable claim, name the single most-tempting *cross-domain* paper family and rule it out; (b)
106 **reserve tier** — keep a "plausibly relevant but secondary" list and pull from it only if a section is
107 genuinely thin, instead of hard-rejecting (reduces over-pruning).
1084. **Re-run citations_lint.py** (the floor + claim-match gate already exists; no new linter needed).
1095. **In data_aware mode**, treat results sentences as citeable **measurement** claims that should cite
110 the dataset/baseline they report on, and **nudge the floor up slightly** (e.g. +4) — real data warrants
111 a denser bibliography.
112
113## Final integrity check (enforced, zero issues required)
114Run python scripts/citations_lint.py <workdir>; it must report **zero** issues:
115- every \cite{key} in sections/*.tex resolves to a refs.bib entry; no orphans; no dup keys;
116- **no stub** entries (missing author/year/venue or DOI/URL) — a hard structural fail that blocks the old
117 title-only fabrication path. NOTE: this is a textual completeness check; it cannot tell a real DOI from an
118 invented one, so a well-formed but fabricated DOI passes the default lint. Existence is enforced by building
119 each entry through doi2bib.py and, **when network is available, by the authoritative existence gate**
120 python scripts/citations_lint.py <workdir> --resolve (opt-in; default OFF to keep offline/sandboxed runs
121 deterministic), which flags doi_unresolved for any DOI that 404s.
122- **every cited key has a claims_map.json entry** with a non-weak support_label (else cite_without_claim_justification / weak_claim_match), and its section field MUST match a section where the key is actually cited (else claims_map_section_mismatch);
123- **per-section coverage**: no evidence-bearing body section with zero cites (section_zero_citations, hard), and every section within its per_section_coverage band (section_coverage_out_of_band, warning — rebalance, don't fabricate). The report's cites_per_section shows the live distribution.
124Run it again after the write stage (cites are inserted there) — the per-section and section-mismatch gates can only be satisfied once the drafted sections/*.tex exist. Then write **logs/2_cite.io.md** — INPUT (claims needing support), DECISIONS (per-claim: provided vs Crossref-fetched vs dropped; the DOIs used), OUTPUT (refs.bib + claims_map.json summary).
125