Read OtherAbstract, and re-test the entries that claim no content - #90
Conversation
A refresh could delete the abstract a cache entry already held. `_parse_abstract` read only `Abstract`, so a record whose abstract PubMed keeps under `OtherAbstract` came back as having no content at all, and the refresh overwrote the cached text with an empty body. `PMID:5697815` went from 1787 to 850 bytes and `PMID:6680124` from 1695 to 650, losing 967 and 1175 characters of real abstract. `OtherAbstract` is where PubMed keeps abstracts contributed by other indexing programs -- PIP, KIE, NASA, AIDS -- mostly on pre-1990 records. They are genuine abstracts, not metadata. Three rules the tests pin: - `Abstract` still wins when a record has both; the fallback is a fallback. - English is preferred. `OtherAbstract` also carries translations, so taking the first element would return the French on a record that has both, and an English snippet would stop matching its own source. - A translation is still used when it is the only text available. Reporting nothing blanks the entry, and the text is at least the paper's own summary. Fixing the extractor does not by itself recover anything, which is the other half of this change. An entry damaged by the bug carries a current `extractor_version`, so `_is_stale_cache_entry` calls it fresh and it is never re-fetched -- the text stays deleted permanently. `ABSENT_CONTENT_CACHE_VERSION` versions the claim "this reference has no content at all", scoped to `content_type: unavailable` exactly as `html_full_text_version` and `xml_extraction_version` are scoped to theirs. Such an entry records what an extractor could *not* find, so an extractor fix can make it wrong; the stamp is what lets a fixed version re-test it. Scoped rather than bumping `EXTRACTOR_CACHE_VERSION` because only entries claiming no content can be wrong in this way. In the dismech cache that is 1,735 files against 57,551 -- a blanket bump would re-fetch every cached reference to find them, and churn on that scale is its own harm. Verified against the four PubMed records from the report: both PIP abstracts come back at full length, and `PMID:4869291` and `PMID:37769103` still correctly report nothing, having no abstract element of any kind. Those two are why the fallback is not simply "never demote to unavailable" -- sometimes the demotion is right, and their old cache entries were storing the MEDLINE citation header as though it were content. Full suite: 1149 passed, 1 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @cmungall's task in 3m 3s —— View job Review: OtherAbstract fallback + absent-content re-test stamp
Verdict: the design is right and the two halves are correctly coupled. The insight that fixing the extractor alone recovers nothing — because a damaged entry carries a current Scoping to I verified the surrounding code statically. I was not able to run the test suite — Findings1. The new stamp isn't documented, unlike the other two. (
2. Some damaged PMID entries are still unreachable, via the content_type = "abstract_only" if abstract else "unavailable"
...
if (content or "").strip() and content_type == "unavailable":
content_type = "summary"When 3. The stamp is emitted from the constant, while the other two are read from
I traced the save paths and this is safe today: 4. Blast radius spans all sources, not just PMID. (informational)
5. Nit: the English-preference loop retries English nodes. ( for node in english + others:English nodes appear twice in the concatenation, so when an English Things I checked that are correct
Test coverageGood, and notably the tests pin the reasons rather than the mechanics. Two gaps worth considering:
|
mypy was the CI failure, not the tests -- 1151 passed on all four Python
versions. `node.get("Language")` can return a list for a multi-valued attribute,
so `.strip()` failed type checking. Handled rather than silenced. I had run bare
`pytest` instead of `just test`, which is pytest + mypy + format.
The reviewer's point about blast radius was right, and the numbers are worse
than stated. Of the 1,735 `unavailable` entries in the dismech cache, 1,499 are
DOI and only 236 are PMID, so most of this refresh is cost rather than repair --
the OtherAbstract fix cannot help a DOI entry. The original claim that "only
entries claiming no content can be wrong in this way" was true but invited the
converse reading. The constant's comment now says so, and rests the design on a
different argument: the stamp is deliberately general -- "this no-content claim
was made by extractor version N" -- which a later fix to any other source will
want, and it remains a 3% refresh against 100%.
The `summary` escape hatch is real. With `source_extra_fields["PMID"]` set, a
record with no abstract is stored as `summary` carrying the extra-fields blob,
so an OtherAbstract-only record damaged by #88 falls outside the stamp and is
never re-tested. dismech does not set that option, so its figures are unaffected,
but the gap is documented in both the staleness rule and troubleshooting.md with
the manual remedy. Not widened to `summary`: most such entries have nothing to do
with a missing abstract, so it would cost a re-fetch of all of them to reach a
config-dependent few.
Also:
- A comment at the emit site records why writing the stamp from the constant is
correct here -- every path that saves an `unavailable` entry has just produced
it with this extractor -- and names the invariant a future enrichment path
would break.
- `absent_content_version` is documented in troubleshooting.md beside the
existing `xml_extraction_version` paragraph, including the one-time refresh.
- The English-preference pass no longer rehearses English nodes in its fallback.
- Two tests added for branches that were only implicitly covered: a
present-but-empty `Abstract` falling through to `OtherAbstract`, and an
`OtherAbstract` carrying no `Language` attribute at all, where the `or "eng"`
default is load-bearing for real PIP records.
just test: 1152 passed, 1 skipped; mypy clean; ruff clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All five findings taken, both test gaps closed, and the CI failure fixed — which was mypy, not the tests. The tests passed. 1151 on every Python version. The single error was Finding 4 is right, and the numbers are worse than either of us had. I checked what the 1,735 entries actually are: So 86% of that refresh is cost, not repair — the OtherAbstract fix cannot help a DOI entry. "Only entries claiming no content can be wrong in this way" was true but invited the converse reading, and the tight-targeting framing in the PR body did not survive contact with the data. The constant's comment now says this outright, and the design rests on a different argument: the stamp is deliberately general — "this no-content claim was made by extractor version N" — which a later fix to DOI or any other source will want. Still a 3% refresh against 100%, so the scoping call stands; my justification for it did not. Finding 2, the I decided against widening the scope to Finding 3 — comment added at the emit site recording why the constant is correct here, and naming the invariant that a future enrichment path would break. Your trace matches mine: no current caller can produce a false certification. Finding 1 — documented beside the Finding 5 — fixed; English nodes no longer appear twice in the fallback pass. Both test gaps added. The On not being able to run the suite — that split the work usefully rather than limiting it. The static read found five real things including the two test gaps; the part you flagged as unverified is exactly the part CI caught.
|
|
Claude finished @cmungall's task in 3m 58s —— View job Re-review after
|
…scoping The comment explaining why `absent_content_version` may be written from the constant named the wrong mechanism. It claimed `_maybe_retry_full_text` "runs on the fetch path where staleness already forced a re-fetch"; it is called inside the `if cached:` branch, which returns without fetching. The conclusion held but the reason did not, and the comment exists precisely to send a maintainer at version 2 to the right invariant. What actually guarantees it: `_load_from_disk` filters stale entries out, so an `unavailable` entry surviving a cache hit already carries the current stamp and re-writing it is a no-op -- and at version 2 a stamp-1 entry is stale, so that branch is never taken for it. The comment now also names `_stale_fallback` and `_preserve_cached_full_text`, the two paths that do hold an untouched cached entry and are safe only because both document that it is not re-saved. Those are the contracts a future change would have to violate. The emitter's scoping had no test. `test_entries_that_do_have_content_are_untouched` exercises the staleness rule, so deleting the `content_type == "unavailable"` guard in `_save_to_disk` left 81 tests green -- verified by removing it. What escapes is not a wrong answer but cache-wide churn: an unconditional stamp adds a line to every entry on refresh, the same problem troubleshooting.md already documents for `full_text_access_type`. `test_an_entry_with_content_is_not_stamped` closes it, and fails against the mutation. Also partitions the OtherAbstract nodes in one pass instead of two. just test: 1153 passed, 1 skipped; mypy clean; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both taken, and the nit. The comment did name the wrong guarantee — you're right. It now states what you traced: The emitter's scoping gap is real — I verified it by removing the guard rather than reasoning about it. 81 tests stayed green. Your framing of what escapes is the part worth keeping: not a wrong answer, but a line added to every cache entry on refresh — the same churn Nit taken: single partition pass.
|
|
Claude finished @cmungall's task in 2m 9s —— View job Re-review after
|
Closes #88.
A refresh could delete the abstract a cache entry already held.
_parse_abstractread only<Abstract>, so a record whose abstract PubMed keeps under<OtherAbstract>came back as having no content at all, and the refresh overwrote the cached text with an empty body.OtherAbstractis where PubMed keeps abstracts contributed by other indexing programs — PIP, KIE, NASA, AIDS — mostly on pre-1990 records. They are genuine abstracts, not metadata.Three rules the tests pin
Abstractstill wins when a record has both. The fallback is a fallback.OtherAbstractalso carries translations, so taking the first element would return the French on a record that has both — and an English snippet would then stop matching its own source.Fixing the extractor recovers nothing on its own
This is the half that is easy to miss. An entry already damaged by this bug carries a current
extractor_version, so_is_stale_cache_entrycalls it fresh and it is never re-fetched. The text stays deleted permanently.ABSENT_CONTENT_CACHE_VERSIONversions the claim "this reference has no content at all", scoped tocontent_type: unavailableexactly ashtml_full_text_versionandxml_extraction_versionare scoped to theirs. Such an entry records what an extractor could not find, so an extractor fix can make it wrong; the stamp is what lets a fixed version re-test it.Scoped rather than bumping
EXTRACTOR_CACHE_VERSIONbecause an entry that records content cannot be wrong in this way. In the dismech cache that is 1,735 files against 57,551 — a blanket bump would re-fetch every cached reference to find them, and churn on that scale is its own harm.Correction (review round 1): the converse does not hold, and an earlier version of this section implied it did.
unavailableis emitted bydoi,url,clinicaltrials,json_api,entrezandppras well aspmid, and theOtherAbstractfix cannot help any of those. Of the 1,735 entries in the dismech cache, 1,499 are DOI and only 236 are PMID, so most of this refresh is cost rather than repair. That is accepted deliberately: the stamp means "this no-content claim was made by extractor version N", which a later fix to any source will want, and it is still a 3% refresh against 100%.One gap, config-dependent: with
source_extra_fields["PMID"]set, a record with no abstract is stored assummarycarrying the extra-fields blob rather than asunavailable, so anOtherAbstract-only record damaged by #88 falls outside this stamp and is never re-tested. dismech does not set that option, so the figures above hold there. It is documented in the staleness rule and introubleshooting.mdwith the manual remedy; widening the scope tosummarywould re-fetch all 2,290 such entries in dismech to reach a config-dependent few.There is a round-trip test for this, because the emitter and the staleness rule have to agree or the fix is inert in either direction: without the emitted stamp every
unavailableentry is re-fetched forever, and without the staleness rule the damaged ones are never re-fetched at all.Why not simply "never demote to unavailable"
Because sometimes the demotion is right. Of the four records that changed in the run that surfaced this, two have no abstract element of any kind:
Those last two had cache entries storing the MEDLINE citation header as though it were content, so
unavailableis the more honest label for them. A blanket protection would have preserved the wrong data.Verification
All four records above checked against live PubMed XML, not fixtures. Full suite: 1149 passed, 1 skipped. Ruff clean, doctests pass.