Stop publishing a plant's taxon id for a bacterium, and gate docs/ (#442) - #477
Conversation
) generate-pages.yaml serves the committed docs/ tree verbatim, and nothing regenerated or checked it. So a data PR that corrected a record and did not re-run `just gen-html` published a page contradicting the KB - invisibly, since no schema validator or id-label gate reads HTML. It had drifted both ways: * 7 pages carried taxon ids the record no longer held. Two rendered NCBITaxon:169215 - the flowering-plant genus Bosea - as a live NCBI link on a bacterium, and KBase_ORT_Workflow_Community_Model still showed the class/phylum ids its record replaced back in 2a3b691. * 7 records had NO page at all, added without a regeneration. That is the failure a diff of existing files cannot see, and it is why the gate counts untracked files and why the missing-page test is separate. Regenerating fixes both. The remaining question the issue raised - whether docs/ should be gated or stop being committed - is answered in favour of gating: the render is 7.35s and deterministic, so a clean checkout that runs it gets a byte-identical tree, and the alternative is "remember to run gen-html", which is what produced the drift. Added: * `just check-docs-current` - regenerate, fail if the tree differs. * .github/workflows/docs-current.yaml. Its paths filter names render.py and render_community_pages.py as FILES: `src/communitymech/render/**` matches nothing, because render is a module, not a package - a filter that matches nothing is a gate that never fires (TraitMech#198 is the same mistake). Checked against the tree rather than assumed. * tests/test_docs_do_not_contradict_the_kb.py - the fast local signal, stating the property rather than the mechanism: a page may not assert a taxon id its record lacks. Checked one-directionally on purpose; the converse is routine, since pages summarise and not every id is rendered. Mutation-checked both ways - restoring the plant id to one page fails that page's case, and moving one page aside fails the missing-page test. Also regenerated reports/gtdb_denominators.tsv, the issue's second artifact. The orphaned NCBITaxon:169215 row is gone and the ORT record's 1033997 is picked up; taxa 578 -> 583. 314 new tests pass; ruff and black clean.
Two HIGH findings, both real, both about the gate not gating. **The gate passed silently when the renderer failed on a record.** render_all caught each per-record exception, printed a ✗, and carried on to print "✅ Rendered 312" and exit 0. A failed record leaves its previous page in place, so the tree has no diff and check-docs-current reported it current - green in exactly the case where the regeneration it gates on had not happened. `>/dev/null` then threw away the only evidence. render_all now returns the failures, prints the true rendered count, and the CLI exits 1; the recipe no longer discards its output. Reproduced with the reviewer's case (an unquoted ISO date, which YAML loads as a date and the template cannot serialise): gate now exits 1 naming the stale page, where before it printed "✅ docs/ matches the KB". Fixing that surfaced a bug in my own patch: I had put `return failed` above the `_generate_index` call, which would have made the browser and landing pages dead code. Caught before commit; the return is now after it. **src/communitymech/templates/** was in no workflow's paths filter.** A one-line edit to community.html rewrites all 312 pages and matched neither this gate's filter nor validate-strict's `src/communitymech/**/*.py`, so it ran neither the gate nor pytest. This is the same trap as the render/** glob the PR already fixed: I verified the listed paths existed, not that they covered what changes the output. Added templates/** and paths.py. **The drift test could not see a swap, and missed an 8th drifted page.** It compared sets - every NCBITaxon id anywhere in the page against every one anywhere in the record - so it passed BioModels_MODEL2405300001_Infant_Gut_HMO_SynCom, which linked *Bacteroides ovatus* to NCBITaxon:821, *Phocaeicola vulgatus*, the row above it. A second live wrong-organism link, the same class as the Bosea case the PR is named after, and 821 is in the record, so the set check was satisfied. Verified the renderer emits taxonomy ids first and in record order across all 312 pages, so the check is now a prefix comparison, row for row. Restoring main's page for that record now fails its case; it did not before. The 7/7 counts in the justfile, the workflow header and the docstring are corrected to 8 pages. **An orphan page survives a record rename**, since gen-html only writes. Both the gate and a new test now catch it; f8d85b1 is a rename of exactly that shape. Filed rather than fixed: #478 (docs/community_umap.html is stale, published, and outside this gate - gen-umap is stochastic so regenerate-and-diff needs a pinned seed) and #479 (the denominators report embeds a machine-local mtime, so it cannot be gated this way). Also corrected from the review: the mutation transcript in the PR body quoted "2 failed, 311 passed" for the missing-page case, which was two mutations applied at once - it is 1 failed, 312 passed. The render is ~9.8s on the reviewer's machine, not 7.35s. 2264 passed, 16 skipped. ruff, black, mypy src/ clean. Gate exits 0.
Review round 1 — addressedBoth HIGH findings were real and both were about the gate not gating. The review also cleared determinism, write-scope, untracked detection, and the "no hand edits" claim by independent experiment, which is what let me treat the rest as settled. 1 (HIGH) — the gate reported green when the renderer failed. Fixed.
Fixing it introduced a bug in my own patch, worth recording: I first put 2 (HIGH) —
|
…297) (#495) #296 rehomed an orphaned note onto a `metabolites` entry reading "Reactive oxygen species detoxification is an inferred interaction process" - a note that says *process* while sitting on a compound. #297 recorded that as a modelling question rather than a defect, and left the choice open: compound, process, or both. Both, because the record already asserted both in prose. CHEBI:26523 is the species; GO:0098869 `cellular oxidant detoxification` is what is done to it. Grounding the process adds no new claim - it makes the claim the note already made machine-readable. GO:0098869 chosen over two nearer-looking terms: * GO:0141082 "symbiont-mediated detoxification of host-generated reactive oxygen species" would assert a host-symbiont relation this consortium does not have - Trichodesmium and Alteromonas are a consortium, and the ROS is photosynthetic rather than host-generated. * GO:0061692 is hydrogen peroxide only, where the record says ROS generally. GO:0098869's definition - "reduces or removes the toxicity of superoxide radicals or hydrogen peroxide" - is the general oxidant case and matches. Verified non-obsolete, canonical label; the id-label gate goes 6094 -> 6095 OK_CANONICAL with no exceptions added. #297 also asks whether the other two "inferred interaction axis" metabolites - phosphate and iron(2+) - want the same treatment. Checked, and the answer is no, for a reason worth recording: clean GO terms exist (GO:0006826 iron ion transport, GO:0006817 phosphate ion transport) but both are *cellular transport* processes, while the record's notes describe ecological *acquisition axes*. Grounding them would introduce a claim the record does not make - the same over-claiming that rules out GO:0141082 above. The ROS case is different precisely because its note already named the process. The new entry inherits the interaction's COMPUTATIONAL evidence; this is the inferred axis the source names, not a measured one. docs/ regenerated - the #477 gate caught the stale page, which is what it is for. 2364 passed, validate-strict 0 errors.
Closes #442.
The gap
generate-pages.yamlpublishes the committeddocs/tree verbatim, and nothing regenerated or checked it. A data PR that corrected a record and didn't re-runjust gen-htmlshipped a page contradicting the KB — invisibly, since no schema validator or id↔label gate reads HTML.It had drifted in both directions:
MSC1_Dominant_CoreandEcoFAB_Ring_Trial_SynCom17renderedNCBITaxon:169215— the flowering-plant genus Bosea — as a live NCBI link on a bacteriumSPRUCE_Peatland_Warming_Community,Chlorella_Keystone_Taxa_Antifungal_SynCom, …The second is the one the issue didn't mention and the more interesting failure: those records were added without a regeneration, so they are simply absent from the published site, and a diff of existing files cannot see it. That's why the gate counts untracked files and why the missing-page test is separate.
KBase_ORT_Workflow_Community_Modelwas also still showing the class/phylum ids (NCBITaxon:2157/1236) its record replaced in 2a3b691.The decision the issue asked for
Gate it. The render is 7.35s and deterministic — a clean checkout that runs it produces a byte-identical tree, which I verified before building anything on the assumption. The alternative is "remember to run
just gen-html", which is precisely what produced the drift.Changes
just check-docs-current— regenerate, fail if the tree differs. Counts untracked files, so a missing page fails too..github/workflows/docs-current.yaml— the CI gate.tests/test_docs_do_not_contradict_the_kb.py— the fast local signal. It states the property rather than the mechanism: a page may not assert a taxon id its record lacks. That distinction matters because the gate can only say "re-run the renderer", while a test failure names the contradiction.reports/gtdb_denominators.tsvregenerated — the issue's second artifact. The orphanedNCBITaxon:169215 Bosearow is gone and the ORT record's1033997is picked up; taxa 578 → 583.A trap I walked into and fixed
My first draft of the workflow filtered on
src/communitymech/render/**.renderis a module (render.py), not a package, so that glob matches nothing — a gate that never fires on renderer changes. This is the same mistake as TraitMech#198, called out inlabel-correspondence.yaml's own header. The paths now name the files, verified against the tree.Verification
Mutation-checked in both directions:
The gate itself was checked the same way: it exits 1 on the drifted tree and 0 once regenerated docs are committed.
ruffandblackclean.Note
docs/is a large diff (20 pages modified, 7 added) because it is 312 generated files catching up on accumulated drift. Every change is renderer output — no hand edits.🤖 Generated with Claude Code