Skip to content

feat: offline / reproducible retractions via Retraction Watch snapshot (v0.2.0)#6

Closed
musharna wants to merge 12 commits into
mainfrom
feat/offline-retraction-watch
Closed

feat: offline / reproducible retractions via Retraction Watch snapshot (v0.2.0)#6
musharna wants to merge 12 commits into
mainfrom
feat/offline-retraction-watch

Conversation

@musharna

Copy link
Copy Markdown
Owner

Summary

Adds a deterministic, no-GPU, offline retraction source — the determinism-axis deepening from the Lane-C plan. ghostcite can now flag retractions from a local Retraction Watch snapshot instead of the live CrossRef updated-by signal, making the retraction tier byte-reproducible and broader-coverage (RW ~43k vs CrossRef ~14k flags).

  • ghostcite fetch-retractions --mailto you@org → downloads the snapshot to the XDG cache with a provenance .meta.json sidecar.
  • --retraction-db <path> pins an exact snapshot (CI-reproducible); --retraction-db none forces live CrossRef; auto-uses the cache if present.
  • When a snapshot is active it is the sole authoritative retraction source (live CrossRef signal skipped), and a retraction fires even when CrossRef can't resolve the DOI (404/unreachable). The report names the active source in text + JSON.
  • Byline/year checks still query CrossRef live — this makes the retraction tier offline/reproducible, not the whole run (full zero-network is a future fixture-cache feature).

New module src/ghostcite/retractions.py (RetractionDB.load/lookup, normalize_doi, default_cache_path, resolve_db, fetch_retractions). Version bump 0.1.0 → 0.2.0.

Provenance / license

Retraction Watch data is publicly available via Crossref Labs but carries no explicit CC0/permissive grant (verified live). ghostcite downloads it on demand and does not redistribute it; NOTICE + README credit Crossref + Retraction Watch (The Center for Scientific Integrity).

Test Plan

  • pytest -m "not live" — 144 passed, ruff + mypy clean, coverage 89%
  • Live API: tests/test_retractions_live.py fetch→load→lookup round-trip against the real Crossref Labs endpoint passes (confirms Wakefield 1998 flagged; runs weekly in live-tests.yml)
  • Real-execution CLI smoke: mailto-required exit 2; --retraction-db flags a DOI CrossRef didn't (incl. the 404 case → RETRACTED + accurate DOI not found, no misleading message); --retraction-db none header

Note: touches README.md in a region near PR #5 (sciwrite-lint positioning) — trivial conflict possible depending on merge order.

🤖 Generated with Claude Code

musharna and others added 12 commits June 11, 2026 22:24
- README: "Offline / reproducible retractions" section (fetch-retractions,
  --retraction-db, snapshot-is-authoritative, byline still live).
- NOTICE: attribute Crossref + Retraction Watch (Center for Scientific
  Integrity); state ghostcite downloads on demand and does not redistribute the
  data (the data carries no explicit CC0/permissive grant — verified live).
- CHANGELOG: 0.2.0 entry.
- Bump version 0.1.0 -> 0.2.0 (pyproject, __init__, version-string tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a DOI is absent from CrossRef (404 or unreachable), the snapshot
lookup was silently skipped, dropping retraction signals the DB knew
about. Decouple the RW DB lookup from CrossRef resolution: if the DB
flags a DOI and CrossRef returned nothing, synthesize a minimal
CanonicalRecord so evaluate() still emits the retraction tier.

Regression test: test_retraction_db_flags_even_when_crossref_404s
(was: exit 0 / no retraction line; now: exit 1 + "RETRACTED per
Retraction Watch").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ride; correct row_count

- Add `retraction_override` kwarg to `evaluate()` so the offline DB can
  assert retraction even when CrossRef returns None (no record), without
  synthesizing a stub CanonicalRecord that triggers the misleading
  "CrossRef record has no author data" UNRESOLVABLE message.
  When canonical is None and override fires, output is RETRACTION + a
  clean UNRESOLVABLE("DOI not found"), not the old spurious author-data
  warning.
- Remove the CanonicalRecord stub construction from cli.py; the rdb path
  now passes retraction_override=(retracted, eoc) to evaluate() directly.
  CanonicalRecord removed from cli.py imports (unused).
- Fix row_count off-by-one in fetch_retractions: bodies without a
  trailing newline were undercounted by 1 (e.g. 2-row body reported 1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@musharna

Copy link
Copy Markdown
Owner Author

Closing — keeping development private until the differentiating moat is built.

@musharna musharna closed this Jun 12, 2026
@musharna
musharna deleted the feat/offline-retraction-watch branch June 12, 2026 03:37
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