Skip to content

feat: the citation gate now reads prose and JSON registers, not just code - #30

Merged
murffious merged 3 commits into
mainfrom
feat/gate-docs-pmids
Sep 8, 2026
Merged

murffious merged 3 commits into
mainfrom
feat/gate-docs-pmids

Conversation

@murffious

Copy link
Copy Markdown
Owner

Why

check_pmids.py scanned src/**.py only. Four of the twelve wrong identifiers found in #25 were on docs/metabolic-constants.md — a page whose entire job is telling a reader which paper backs a constant — in exactly the blind spot the gate did not cover. There were 39 PubMed ids across docs/ that nothing read.

Three lanes, each at the strictness it can actually carry

Lane Source Test
code a dict with pmid + citation title must appear in the citation
json an object with pmid + title in a register title must match, containment either way
prose a PubMed id in markdown title or first-author surname in surrounding lines

Two of those need justifying.

JSON containment runs both directions because several stored titles are truncated mid-word by whatever wrote them, so the stored string has to be the opening of the resolved one. Length-guarded at 40 characters so a stub cannot match by accident.

Prose earns the looser test. Docs abbreviate titles. non-laws.md writes "A linear steady-state treatment of enzymatic chains." for a title that runs on to "General properties, control and effector strength." Title containment therefore false-positives on correct citations, and a noisy gate gets switched off inside a week — the lesson already written into check_curies.py's own docstring about a 27% false-positive rate.

The surname is the discriminating field, and it is not a weaker check for the failure that actually happened. Every one of the four wrong doc ids named a paper by an unrelated author:

Claimed Resolves to
Miller & Wolin Buelens, Differential effects of interleukin-10…
Hinkle Usuki, Covalent linkage between nucleotides and PD-ECGF
Hinkle Warren, Cost-utility analysis of imatinib mesylate…
St-Pierre Sun, Changes of inflammatory factors in coronary artery disease

All four fail the surname test.

Exceptions, with a written reason each

tools/pmid_docs.allow, same shape as tools/separation.allow. Two kinds, deliberately not equivalent:

  • <pmid>@<path> — skipped, in that one file. For ids the prose quotes on purpose as being wrong: the correction tables that record what an identifier used to be. Scoped, so the same wrong id used as a real citation on another page is still caught. I verified that specifically.
  • <path> — existence-only. Bare reference lists (VMH reaction ids with no author anywhere on the line) and pages citing by internal record id, where the authoritative pair lives in the JSON register and is checked strictly there. The id is still resolved, so a dead identifier like the one that resolved to no PubMed record at all is still caught. Only the "does it name this paper" half is dropped, because there is no name.

Adding a line is a claim that an id cannot be checked, not that it is fine.

A coupling bug this exposed and fixes

check_constants.py requires every grounded constant's id to be in the cache. But grounded(...) is a call, not a dict, so the harvester never saw it — and running --update-cache dropped PMID 15620362, after which the constants gate failed on an id it could no longer resolve. I hit this for real mid-change.

grounded() ids are harvested now, existence-only since supports says what a paper backs rather than what it is called. One harvester, one cache, no drift between the two gates. Verified: a full --update-cache rebuild leaves check_constants --strict green.

Verification

Not by inspection. A wrong id was planted in each lane and watched to fail, then reverted.

94 shipped citation(s) [code + prose]; 46 confirmed, 0 mismatched,
48 existence-only, 17 quoted-deliberately

--code-only is kept for a fast path: 31 citations, 31 confirmed.

Test plan

Check Result
pytest tests/ -q 630 passed, 1 skipped, 5 xfailed
check_pmids --offline --strict pass, 0 mismatched
check_pmids --offline --strict with a planted wrong id exit 1
check_constants --strict after a full cache rebuild pass
check_no_human_rows / check_third_party --strict / check_separation pass
ruff check pass
mkdocs build --strict pass

🤖 Generated with Claude Code

murffious and others added 3 commits September 7, 2026 20:29
…code

check_pmids scanned `src/**.py` only. **Four of the twelve wrong ids found on
2026-09-07 were on `docs/metabolic-constants.md`**, in exactly the blind spot the
gate did not cover — a page whose entire job was telling a reader which paper
backed a constant. 39 identifiers in docs/ were unread by anything.

Three lanes now, checked at the strictness each one can actually carry:

  code    a dict with `pmid` + `citation`. Title must appear in the citation.
  json    an object with `pmid` + `title` in a register. Title must match, with
          containment run either way because several stored titles are truncated
          mid-word by whatever wrote them.
  prose   a PubMed id in markdown. Title OR first-author surname must appear in
          the surrounding lines.

Prose earns the looser test. Docs abbreviate titles — "A linear steady-state
treatment of enzymatic chains." for a title that runs on — so title containment
false-positives on correct citations, and a noisy gate gets switched off in a
week (the lesson written into check_curies.py's own docstring). The surname is
the discriminating field: every one of the four wrong doc ids named a paper by an
unrelated author and fails here.

Exceptions in `tools/pmid_docs.allow`, one written reason each, same shape as
`tools/separation.allow`. Two kinds, and they are not equivalent:

  <pmid>@<path>  skipped, in that one file. For ids the prose quotes ON PURPOSE
                 as wrong — the correction tables. Scoped, so the same id used as
                 a real citation on another page is still caught.
  <path>         existence-only. Bare reference lists (VMH reaction ids) and
                 pages citing by internal record id, where the authoritative
                 pmid+title pair lives in the JSON register and is checked
                 strictly there. The id is still resolved, so a dead identifier
                 like the one resolving to no record at all is still caught.

Also fixes a coupling bug this exposed. `check_constants.py` requires every
grounded constant's id to be in the cache, but `grounded(...)` is a call, not a
dict, so the harvester never saw it — and `--update-cache` dropped PMID 15620362,
after which the constants gate failed on an id it could no longer resolve.
`grounded()` ids are harvested now, existence-only since `supports` says what a
paper backs rather than what it is called. One harvester, one cache, no drift.

Verified by planting a wrong id in each lane and watching it fail, including an
allow-listed id reused on a different page:

  94 citations [code + prose]; 46 confirmed, 0 mismatched, 48 existence-only,
  17 quoted-deliberately

Test plan: 630 passed, 1 skipped, 5 xfailed. check_constants still green after a
full `--update-cache` rebuild, which is the regression above. ruff, separation,
third-party, human-rows and `mkdocs build --strict` all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@murffious
murffious merged commit 944a598 into main Sep 8, 2026
7 checks passed
@murffious
murffious deleted the feat/gate-docs-pmids branch September 8, 2026 02:47
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