ts-paper-cite — real, complete citations (no fabrication)
Citations support real claims with real evidence. Every entry in refs.bib is a paper that
actually exists and that you verified. This stage replaces the original 100+-call retrieve→triage→
slot→bind→postdraft engine (which fabricated title-only stubs to hit hardcoded quotas) with a few
Claude reasoning passes + native web search.
Procedure — broad systematic retrieval, then triage (adopt the original engine's coverage, keep integrity)
A 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.
- 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.
- 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:
- the core task and its canonical benchmark/problem papers;
- each method component / technique the paper builds on (every named module, predictor, feature, loss);
- every dataset and every metric named in the experiment design;
- every baseline named;
- the adjacent lineages a reviewer would expect (neighbouring tasks, the families your method departs from);
- recent surveys / foundational works of the area.
- 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.
- 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:
- triage: same-task / same-technical-lineage (keep) vs cross-domain / loose-overlap (reject). Surfacing many and filtering is better than finding the minimum.
- 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".
Log kept/rejected counts per angle in retrieval_plan.md.
- 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.
- 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.
- Write
refs.bib (complete entries only) + claims_map.json, then run the linter (which now enforces a coverage floor).
Every entry must be complete and real
- 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.
- 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.
- 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.
- One bibkey per real paper (
firstauthorYEARkeyword, ASCII, no leading digit); reuse it everywhere; never duplicate a paper under two keys.
- 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.
- 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.
How many — coverage target met with REAL papers (not a fabrication quota), per the TEMPLATE
Coverage is template-driven and enforced on TWO axes by citations_lint.py — a global count
and a per-section distribution. The global floor alone is NOT sufficient: a paper can clear 40
total cites while whole body sections carry zero and related_work overflows its band. Both axes read
template.json's citations block:
- Global floor (
citations.floor; TS ts_iieta = 40, target ~40–50; neurips = 30) →
coverage_below_floor if under.
- Per-section bands (
citations.per_section_coverage, e.g. related_work:[20,30],
introduction:[8,12]) → an evidence-bearing body section (introduction/related_work/method/
experiments/analysis) with zero cites is a HARD error section_zero_citations; a non-empty
section outside its band is a warning section_coverage_out_of_band (rebalance — move/merge
real cites; never fabricate or delete real support). Citation-free sections in a proposal
(abstract, conclusion, future_work) are exempt from the zero-error.
A well-read paper sits at/above the floor and spreads its evidence across every body section. The
only way to satisfy both is to search more broadly for real papers and place each in the section
whose claim it actually supports — never to invent, pad, or dump everything into related_work.
Stub/incomplete entries hard-fail separately, so coverage can only be cleared with real, fully-specified
papers ("search harder", never "fabricate"). The seed from ts-idea2story (step 0) counts toward this
once each entry is verified.
Per-section coverage (real papers; expand the search until each is genuinely covered):
- related_work — the heaviest: map the full lineage of every theme (typically 20–30 distinct works), so a reviewer sees you command the field.
- introduction — solid background + every prior-method attack cited (8–12).
- method — cite every technique/module/predictor/feature/loss you build on or contrast (one per real predecessor).
- experiments — cite every dataset, every metric, and every baseline named.
- abstract/conclusion — none.
To 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.
IIETA in-text style (used by the write stage)
Numeric [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.
Honesty (proposal — no real results)
Cite 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.
Emit claims_map.json (enforced claim↔citation matching)
Alongside refs.bib, write claims_map.json mapping every bibkey you will cite to its justification:
{ "chung2016syncnet": {"claim": "lip-sync models detect audio-video offset by contrasting windows",
"support_label": "same_line_support", "section": "related_work"} }
support_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.
Post-draft saturation sweep (REQUIRED after write/refine — it is now gated)
Citations are inserted in the write stage, so the per-section bands above are only meaningful after
the draft exists. Re-running citations_lint.py on the drafted sections/*.tex is mandatory: it now
fails on any evidence-bearing section left with zero cites (section_zero_citations) and on any
claims_map section that disagrees with where the key is actually cited
(claims_map_section_mismatch). Do a positive pass to catch citeable sentences the draft introduced
that carry no cite — without porting the product's heavyweight ledger/cluster engine (the per-section
gate distills its essential purpose deterministically).
- Find under-cited sections. Scan each finalized section; flag any factual claim about prior work /
datasets / baselines / metrics with no
\cite. (Self-method, self-results, and future-work sentences
are non-citeable — never bolt a prior-work cite onto a results sentence.)
- Fill with real papers only, via the same WebSearch +
doi2bib.py + claims_map.json discipline;
honesty-by-omission if no real paper fits. At most 3–4 new cites per rewritten paragraph; preserve
every existing \cite exactly.
- Two cheap precision devices (Claude-native, no code): (a) name the nearest wrong neighbor — for
each citeable claim, name the single most-tempting cross-domain paper family and rule it out; (b)
reserve tier — keep a "plausibly relevant but secondary" list and pull from it only if a section is
genuinely thin, instead of hard-rejecting (reduces over-pruning).
- Re-run
citations_lint.py (the floor + claim-match gate already exists; no new linter needed).
- In
data_aware mode, treat results sentences as citeable measurement claims that should cite
the dataset/baseline they report on, and nudge the floor up slightly (e.g. +4) — real data warrants
a denser bibliography.
Final integrity check (enforced, zero issues required)
Run python scripts/citations_lint.py <workdir>; it must report zero issues:
- every
\cite{key} in sections/*.tex resolves to a refs.bib entry; no orphans; no dup keys;
- no stub entries (missing author/year/venue or DOI/URL) — a hard structural fail that blocks the old
title-only fabrication path. NOTE: this is a textual completeness check; it cannot tell a real DOI from an
invented one, so a well-formed but fabricated DOI passes the default lint. Existence is enforced by building
each entry through
doi2bib.py and, when network is available, by the authoritative existence gate
python scripts/citations_lint.py <workdir> --resolve (opt-in; default OFF to keep offline/sandboxed runs
deterministic), which flags doi_unresolved for any DOI that 404s.
- 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);
- 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.
Run 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).
1---2name: ts-paper-cite3description: Stage 2 of the ts-paper suite. Build a complete, REAL refs.bib for a Traitement du Signal paper — using the user's provided references first and Claude's native WebSearch/WebFetch only for genuine gaps. Every entry must have full metadata (authors, year, venue, vol(issue):pages, DOI); never a title-only stub; reference count is driven by real evidence, not a quota. Use when assembling or fixing the bibliography of a TS paper, or whenever citations must be real and complete.4---56# ts-paper-cite — real, complete citations (no fabrication)78Citations support real claims with real evidence. **Every entry in `refs.bib` is a paper that9actually exists and that you verified.** This stage replaces the original 100+-call retrieve→triage→10slot→bind→postdraft engine (which fabricated title-only stubs to hit hardcoded quotas) with a few11Claude reasoning passes + native web search.1213## Procedure — broad systematic retrieval, then triage (adopt the original engine's *coverage*, keep integrity)14A 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.15160. **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.171. **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`**:18 - the **core task** and its canonical benchmark/problem papers;19 - **each method component / technique** the paper builds on (every named module, predictor, feature, loss);20 - **every dataset and every metric** named in the experiment design;21 - **every baseline** named;22 - the **adjacent lineages** a reviewer would expect (neighbouring tasks, the families your method departs from);23 - recent **surveys / foundational** works of the area.242. **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.253. **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`:26 - **triage**: same-task / same-technical-lineage (keep) vs cross-domain / loose-overlap (reject). Surfacing many and filtering is better than finding the minimum.27 - **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".28 Log kept/rejected counts per angle in `retrieval_plan.md`.294. **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.305. **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.316. **Write `refs.bib`** (complete entries only) + `claims_map.json`, then run the linter (which now enforces a coverage floor).3233## Every entry must be complete and real34- 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.**35- 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.36- 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.37- One bibkey per real paper (`firstauthorYEARkeyword`, ASCII, no leading digit); reuse it everywhere; never duplicate a paper under two keys.38- **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.39- **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`.4041## How many — coverage target met with REAL papers (not a fabrication quota), per the TEMPLATE42Coverage is **template-driven and enforced on TWO axes** by `citations_lint.py` — a global count43**and** a per-section distribution. The global floor alone is NOT sufficient: a paper can clear 4044total cites while whole body sections carry zero and related_work overflows its band. Both axes read45`template.json`'s `citations` block:46- **Global floor** (`citations.floor`; TS `ts_iieta` = **40**, target ~40–50; `neurips` = **30**) →47 `coverage_below_floor` if under.48- **Per-section bands** (`citations.per_section_coverage`, e.g. `related_work:[20,30]`,49 `introduction:[8,12]`) → an evidence-bearing body section (introduction/related_work/method/50 experiments/analysis) with **zero** cites is a **HARD error** `section_zero_citations`; a non-empty51 section **outside its band** is a **warning** `section_coverage_out_of_band` (rebalance — move/merge52 real cites; never fabricate or delete real support). Citation-free sections in a proposal53 (abstract, conclusion, future_work) are exempt from the zero-error.5455A well-read paper sits at/above the floor **and** spreads its evidence across every body section. The56**only** way to satisfy both is to *search more broadly for real papers and place each in the section57whose claim it actually supports* — never to invent, pad, or dump everything into related_work.58Stub/incomplete entries hard-fail separately, so coverage can only be cleared with real, fully-specified59papers ("search harder", never "fabricate"). The seed from ts-idea2story (step 0) counts toward this60once each entry is verified.6162Per-section coverage (real papers; expand the search until each is genuinely covered):63- **related_work** — the heaviest: map the *full lineage* of every theme (typically **20–30** distinct works), so a reviewer sees you command the field.64- **introduction** — solid background + every prior-method attack cited (**8–12**).65- **method** — cite **every** technique/module/predictor/feature/loss you build on or contrast (one per real predecessor).66- **experiments** — cite **every** dataset, **every** metric, and **every** baseline named.67- abstract/conclusion — none.6869To 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.7071## IIETA in-text style (used by the write stage)72Numeric `[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.7374## Honesty (proposal — no real results)75Cite 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.7677## Emit `claims_map.json` (enforced claim↔citation matching)78Alongside `refs.bib`, write **`claims_map.json`** mapping every bibkey you will cite to its justification:79```json80{ "chung2016syncnet": {"claim": "lip-sync models detect audio-video offset by contrasting windows",81 "support_label": "same_line_support", "section": "related_work"} }82```83`support_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.8485## Post-draft saturation sweep (REQUIRED after write/refine — it is now gated)86Citations are inserted in the write stage, so the per-section bands above are only meaningful **after87the draft exists**. Re-running `citations_lint.py` on the drafted `sections/*.tex` is mandatory: it now88fails on any evidence-bearing section left with zero cites (`section_zero_citations`) and on any89`claims_map` `section` that disagrees with where the key is actually cited90(`claims_map_section_mismatch`). Do a **positive** pass to catch citeable sentences the draft introduced91that carry no cite — without porting the product's heavyweight ledger/cluster engine (the per-section92gate distills its essential purpose deterministically).931. **Find under-cited sections.** Scan each finalized section; flag any *factual claim about prior work /94 datasets / baselines / metrics* with no `\cite`. (Self-method, self-results, and future-work sentences95 are non-citeable — never bolt a prior-work cite onto a results sentence.)962. **Fill with real papers only**, via the same WebSearch + `doi2bib.py` + `claims_map.json` discipline;97 honesty-by-omission if no real paper fits. At most 3–4 new cites per rewritten paragraph; preserve98 every existing `\cite` exactly.993. **Two cheap precision devices (Claude-native, no code):** (a) **name the nearest wrong neighbor** — for100 each citeable claim, name the single most-tempting *cross-domain* paper family and rule it out; (b)101 **reserve tier** — keep a "plausibly relevant but secondary" list and pull from it only if a section is102 genuinely thin, instead of hard-rejecting (reduces over-pruning).1034. **Re-run `citations_lint.py`** (the floor + claim-match gate already exists; no new linter needed).1045. **In `data_aware` mode**, treat results sentences as citeable **measurement** claims that should cite105 the dataset/baseline they report on, and **nudge the floor up slightly** (e.g. +4) — real data warrants106 a denser bibliography.107108## Final integrity check (enforced, zero issues required)109Run `python scripts/citations_lint.py <workdir>`; it must report **zero** issues:110- every `\cite{key}` in `sections/*.tex` resolves to a `refs.bib` entry; no orphans; no dup keys;111- **no stub** entries (missing author/year/venue or DOI/URL) — a hard structural fail that blocks the old112 title-only fabrication path. NOTE: this is a textual completeness check; it cannot tell a real DOI from an113 invented one, so a well-formed but fabricated DOI passes the default lint. Existence is enforced by building114 each entry through `doi2bib.py` and, **when network is available, by the authoritative existence gate**115 `python scripts/citations_lint.py <workdir> --resolve` (opt-in; default OFF to keep offline/sandboxed runs116 deterministic), which flags `doi_unresolved` for any DOI that 404s.117- **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`);118- **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.119Run 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).