Verify pre-print licences instead of assuming CC-BY; make every shipped skill findable#25
Open
lfnothias wants to merge 13 commits into
Open
Verify pre-print licences instead of assuming CC-BY; make every shipped skill findable#25lfnothias wants to merge 13 commits into
lfnothias wants to merge 13 commits into
Conversation
lfnothias
marked this pull request as ready for review
July 10, 2026 11:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two follow-ups from #22, kept general so they work for any collection and any future expansion. Stacked on
consolidate/2026-07-10; retarget tomainonce #22 lands.1. Pre-prints are not always CC-BY
OPEN_ACCESS_POLICY.mdasserted that pre-prints are "always CC-BY by repository policy". bioRxiv, medRxiv and arXiv all let the author choose, so the rule could admit text we have no right to redistribute.scripts/preprint_license.pyresolves the real posting licence. It reads pre-print-ness from the registry's own declaration — Crossreftype: posted-content, DataCiteresourceTypeGeneral: Preprint— never from a DOI prefix. A prefix names a registrant, not a work type:10.1101is Cold Spring Harbor, covering bioRxiv, medRxiv and CSHL journals, and bioRxiv now also registers under openRxiv's10.64898. Any server either registry covers therefore resolves with no code change.Where a registry declares no usable licence, it falls back to the server's own API, routed by the
institution[].namethe registry itself reports. Servers and licence tokens live ingovernance/preprint_servers.yaml; adding one is a data edit.Three corrections to the guidance in #22, all established by calling the live APIs:
arXivRaw). arXiv's defaultnonexclusive-distribgrants no reuse rights.403. Resolve it through Crossref, which carries its licence.license[].URLat a FAQ page, not a deed. That is an unknown, not a licence.A second licence axis, because the existing one asks a different question
license_tieranswers "what may I do with the tool?" —openmeans commercial use is fine. A paper's licence answers "may we redistribute the source?". These disagree: CC-BY-ND permits commercial use but forbids derivative text, so reading it through the tool tier would call it open. Mapping paper licences ontolicense_tier, as originally proposed, is a category error.So
governance/license_tiers.yamlgrows a second canonical table,source_reuse(full/limited/none). AddingCC-BY-ND-4.0andCC-BY-NC-ND-4.0to the tool map changes no behaviour — both previously reached the same answer through the fallback — it only makes them explicit.An unlisted licence returns
None, never a default.None(we failed to establish the rights) andnone(the rights were withheld) are different answers and the code refuses to collapse them.Audit of what is already shipped — reported, not dropped
Of 600 corpus DOIs, 56 are pre-prints. Only 15 are full-reuse; 32 are NC/ND; 9 grant no reuse at all (8 bioRxiv
cc_no, 1 arXiv default) and arestatus: included. Full findings, the 9 entries, and 24 malformed DOIs the audit surfaced are in #24. No entry was removed and no gate was weakened — every one of the 56 was admitted onrepo-oa, not on the pre-print rule, so this is the sameaccess.typeconflation tracked in #23.A rate-limited registry used to be indistinguishable from a missing DOI, which silently marked resolvable pre-prints
unresolved.fetch_jsonnow retries429/5xxwith backoff and honoursRetry-After; only403/404/410means "no record". That mistake inflated the first audit run by 2 entries.2. A skill that ships must be findable
collections/metabolomics/v2/skills/masster/SKILL.mdexisted on disk but appeared in none ofskills_index.json,kb_bundle.jsonordocs-site/search_index.json— invisible to search, the MCP server and the docs site.The cause is structural.
propagate_license_tiers.pyjoins tiers onto entries the index already has, keyed by DOI; it never adds one. Masster has no DOI and no corpus entry, so it could never enter by that route. Andcheck_license_tiers.pyonly cross-checks skills already indexed, so CI never noticed.Chosen fix: derive the index from the skills directory (option a), not admit non-OA tools into the paper corpus (option b).
corpus.yamlis a paper corpus keyed by DOI andaccess.type; a tool with no paper has no source to redistribute, so gating its discoverability on an open-access field is wrong by construction. Option (b) would have coupled discovery to the legal gate and let non-OA content into the corpusverify-paperpolices — weakening a gate the brief forbids weakening.scripts/skill_index.pyderives an entry from a skill's own frontmatter, and a new CI gate fails when a skill exists on disk but in no index. Skills are excluded by declarative property — an_-prefixed directory (the conventionrelease_gate.pyalready uses) ormetadata.role: meta— never by a hardcoded name list.The acknowledgment gate is preserved:
--fixrefuses to index a skill with nometadata.license_tier, so a non-open skill can never become searchable while shedding the label that governs its use. Masster shipslicense_tier: noncommercialwithrequires_ack: true, andcheck_license_tiers.pynow cross-checks it because it is finally visible.A second orphan, found on the way — larger than it first looked
docs-site/build_search_index.pysplit frontmatter on the substring---, so any skill whose frontmatter contained a rule was truncated. 41 of 7,722 skills parsed differently, not the 2 I first reported:mzml-data-parsing-pymzml-pyopenmsin v1 and v2, whose evidence span contains-----. YAML failed, and the skill vanished from the index.provenance,attributionandclaimsinto the body — which then polluted theirsearch_textandsummary. Those 39 were mis-indexed, not missing, which is why a naive count found only 2.Verified with a parser-vs-parser diff over all 7,722 files: 41 changed, zero regressions (no file lost a key or gained pollution). Both consumers now share one canonical
split_frontmatter().Search index:
7718 -> 7722, exactly the 7,722 SKILL.md on disk. The four new entries are the 2 recovered skills,masster, andasb-metabolomics(the committed index predated it). Zero removals; papers and tools unchanged.Generality
The maintainer asked that these stay general for library and collection expansion. Enforced by tests, not by intent:
test_no_doi_literals_in_scriptswalks the AST of everyscripts/*.pyand fails on any DOI-shaped string constant outside a docstring.test_preprint_detection_never_dispatches_on_a_doi_prefixfails if a10.literal re-enters the resolver.Each guard was mutation-tested: reintroducing the bug turns the test red.
Verification
285 passed(was 253; +32 new, no regressions)python -m scripts.skill_indexexits 1 namingmassterbefore the fix, 0 aftercheck_license_tiers.pygreen; description-discipline lint green over 7,722 skillsReview findings folded in
Two independent adversarial reviews. What changed:
One blocking defect, in the direction that matters.
_first_recognised_spdxreturned the first licence a registry declared. Crossref lists the accepted manuscript and the version of record separately and does not guarantee their order, so a work declaring both CC-BY and CC-BY-NC-ND resolved to whichever came first —admissible_as_open_accessflipped on array position. Acontent-version: tdmentry (a text-mining grant, not a redistribution grant) was also read as a licence to reuse. Both reproduced, then fixed: the most restrictive declared licence now governs, TDM grants are excluded, and a recognised SPDX with nosource_reuserow blocks rather than falling through to a permissive sibling.A licence deed quoted inside an unrelated URL was accepted.
https://evil.example.com/redirect?to=creativecommons.org/licenses/by/4.0resolved to CC-BY. The host is now checked before the path.The gate could inspect nothing and pass. Run from the wrong directory,
skill_indexprintedPASShaving visited zero collections. It now fails when no skill directory matches.packs/was unguarded — 4,961 shipped skills with akb_bundle.jsonthe gate never visited, able to reproduce the exact bug this PR fixes. Now covered; the gate walks 12 directories.The tier vocabulary had three copies (
license_tiers.yaml,check_license_tiers.py, and the newskill_index.py). Both scripts now read the canonical map, so adding a tier is a data edit.Also:
kb_slugsare derived from a skill's DOIs instead of written empty; a malformed registry payload fails its own DOI rather than aborting a bulk audit; andunresolvedis no longer cached, so a transient outage cannot freeze into a permanent verdict.I did not accept one finding:
10.64898/…is a genuine bioRxiv DOI (prefix registered to openRxiv), verified against Crossref's prefix API andapi.biorxiv.org.Re-resolving all 15 previously-
fullpre-prints under most-restrictive-wins changed none of them, so the numbers in #24 stand.Verification
303 passed(was 253 on the base; +50 new, no regressions)skill_indexexits 1 namingmassterbefore the fix, 0 after, across 12 directoriesCI note
This PR is based on
consolidate/2026-07-10(#22), andvalidate.yml/verify-paper.ymlonly trigger onpull_request: branches: [main]. No CI runs on this PR until #22 merges and this retargets tomain. Both gates were therefore replayed locally by extracting and executing the workflows' own inline scripts:PASS: description discipline OK (7722 skill files checked)andPASS: Paper access-tier validation OK (684 included papers checked).