Skip to content

fix: force co-authors drawn into a DOI conflict by re-enrichment - #60

Merged
gabrielspadon merged 1 commit into
mainfrom
fix/refresh-doi-conflict-cascade
Oct 25, 2026
Merged

gabrielspadon merged 1 commit into
mainfrom
fix/refresh-doi-conflict-cascade

Conversation

@gabrielspadon

Copy link
Copy Markdown
Collaborator

Summary

The 2026-09-01 and 2026-10-01 scheduled refreshes both failed in finalize_run with conflicting copies of the same DOI remain in author directories: Milios (ME8aQywAAAAJ), Oore (cI0dYX4AAAAJ), Sajjad (t3BH6NkAAAAJ).

run_all forces enrichment for author directories that hold conflicting copies of one DOI, but it chose that set once, before any author ran. Forcing rewrites the author's copy of every DOI it holds, while a co-author's complete copy of the same DOI is skipped (process_article complete-entry skip). A forced author can therefore open a conflict that finalization rejects. The failed run is never committed, so the same corpus produced the same failure each month.

Mechanism, from the committed corpus

  • The preflight set is Haque, Oore and Trappenberg (10.1109/tmlcn.2025.3575368 and 10.52202/068431-2156 disagree on title, authors and year).
  • Re-enriching Oore rewrites its SugarCrepe++ record (10.52202/079017-0571) with Crossref's author order, Dumpala, Jaiswal, Sastry, Milios, Oore, Sajjad.
  • output/Milios (ME8aQywAAAAJ)/Dumpala2024-SensitivityGenerative.bib and output/Sajjad (t3BH6NkAAAAJ)/Dumpala2024-SensitivityGenerative.bib are complete, so they are skipped and keep Sastry last. That leaves three copies with two author orders.

Fix

run_all re-checks after each pass and forces the authors a pass drew into a conflict. Each author is forced at most once, so the loop ends within one pass per author. The finalization check is unchanged.

Verification

  • pytest -m 'not live' --cov=citeforge --cov=main --cov-fail-under=68 on Python 3.12, macOS: 2078 passed, 2 skipped (both the documented cutover contracts in tests/test_workflow_contracts.py), 7 deselected, coverage 84.14%.
  • ruff check citeforge/ tests/ main.py and mypy citeforge/ main.py are clean.
  • The new test test_run_all_forces_coauthors_whose_skipped_copy_a_forced_author_rewrote fails on main with Milios and Oore left conflicting, and it passes with this change.
  • Replay on a copy of the committed corpus, driving the real run_all with live Crossref, arXiv, OpenAlex and DOI enrichment of shared-DOI records. Scholar and Gemini were not used. On main the replay forced 3 authors and finalize_run raised the production error verbatim. With this change it forced 9 authors over successive passes (Milios, Sajjad, then Rudzicz, Zeh, Gagie and He), left zero conflicts, and finalize_run completed.

Review before the next refresh merges

In that replay, every forced copy of SugarCrepe++ also replaced the VISLA Benchmark preprint (arXiv 2404.16365, a separate paper from SugarCrepe++, arXiv 2406.11171). The replacement comes from PREPRINT_PAIR in citeforge/identity.py:243, at title similarity 0.713 and composite 0.662. Today only Oore's copy is affected. With this change the next refresh also drops VISLA from Milios, Sajjad and a2i2. This is a separate identity defect, and it is not fixed here.

The monthly refresh forces enrichment for author directories that hold
conflicting copies of one DOI, but it chose that set once, before any
author ran. Forcing rewrites the author's copy of every DOI it holds,
while a co-author's complete copy of the same DOI is skipped, so a forced
author can open a conflict that finalization then rejects.

The 2026-09 and 2026-10 runs forced Haque, Oore and Trappenberg. Oore's
re-enriched SugarCrepe++ record (10.52202/079017-0571) took Crossref's
author order while Milios and Sajjad kept their skipped copies, and
finalize_run failed on Milios, Oore and Sajjad both months.

run_all now re-checks after each pass and forces the authors a pass drew
into a conflict, each at most once, so the loop ends within one pass per
author.
@gabrielspadon
gabrielspadon merged commit 465b673 into main Oct 25, 2026
7 checks passed
@gabrielspadon
gabrielspadon deleted the fix/refresh-doi-conflict-cascade branch October 25, 2026 02:03
gabrielspadon added a commit that referenced this pull request Oct 25, 2026
## Summary

The identity check treated the published SugarCrepe++ record
(10.52202/079017-0571, arXiv 2406.11171) and the VISLA Benchmark
preprint (10.48550/arxiv.2404.16365) as one work. They matched at a
title score of 0.713. During the refresh replay this deleted VISLA from
Milios, Oore, Sajjad and a2i2.

## Root cause

`_disk_identity` rejected a preprint/published pair only when both
records carried arXiv ids that differed (`citeforge/identity.py`, the
preprint-pair branch). `merge_utils` strips `eprint` and `archiveprefix`
from every record that has a published DOI, so a published record on
disk never carries an arXiv id and the guard could not fire. The pair
then fell through to `SIM_PREPRINT_TITLE_THRESHOLD` (0.55), which exists
for retitled preprints. `_enrichment_identity` had the same gap in its
composite gate: a Crossref candidate carries no arXiv id either, and
VISLA vs SugarCrepe++ scored COMPOSITE True.

## Fix

If only one record carries an arXiv id, that now counts as unknown
identity rather than agreement. Unless both records carry the same arXiv
id, the disk preprint-pair, composite and relaxed-preprint paths require
`SIM_FILE_DUPLICATE_THRESHOLD` (0.95), and the enrichment composite gate
stays closed. Identical-title twins, DOI matches, DOI versions, equal
arXiv ids, external ids and truncated titles are unchanged.

## Effect on the committed corpus

I evaluated every same-directory pair under `DISK_SURVIVOR` before and
after the change. Five pairs flip from "same work" to "distinct":

- VISLA vs SugarCrepe++ in Milios, Oore and Sajjad. These are distinct
works and are now kept.
- Oore, `Lowe2023-ZeroShot.bib` (2023 workshop paper) vs
`Lowe2024-EmpiricalStudy.bib` (arXiv 2406.02465, TMLR 2026). These are
distinct titles and are now kept.
- Reilly, `Porcino2022-DesignXR.bib` (arXiv 2208.14842, abbreviated
title) vs `Porcino2022-DesignExtended.bib`
(10.1007/978-3-031-20212-4_9). This is likely the same paper, but
neither an arXiv DOI nor a Crossref relation links them, so the preprint
is kept rather than removed as superseded.

## Verification

- `pytest -m 'not live' --cov=citeforge --cov=main --cov-fail-under=68`
on Python 3.12, macOS: 2080 passed, 2 skipped (the documented cutover
contracts in `tests/test_workflow_contracts.py`), 7 deselected, coverage
84.13%.
- `ruff check citeforge/ tests/ main.py` and `mypy citeforge/ main.py`
are clean.
- `test_published_record_never_absorbs_a_distinct_arxiv_preprint` uses
the committed VISLA and SugarCrepe++ records. Both directions fail on
`main`: the incoming published record replaces VISLA, and an incoming
VISLA is refused. Both pass with this change.
-
`test_enrichment_never_adopts_a_distinct_work_as_the_published_version`
fails on `main` with COMPOSITE True and passes with this change.

The refresh replay on `main` plus #60 and this change is reported in a
comment below.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant