Repository navigation
Licence per copy, one shared bibliography; derived redistribution (study-schema-license) - #41
Conversation
A licence belongs to a copy. A copy is a route (pmc, publisher,
repository, preprint_server, unrecorded), a version (vor, am, submitted)
and a License {code, basis stated|assumed, stated_by, stated_as}; a copy
we hold adds its access (public, tdm_grant, unrecorded).
ParsedPaper.text_copy is the copy the text was read from, and
Bibliography.reported_copies the copies a source lists that we do not
hold. publisher-tdm is no longer a licence code: a text-mining grant is
HeldCopy.access = tdm_grant, beside the copy's own licence.
Bibliography.license becomes a required License: the version of
record's, or the preprint's for a preprint-only paper. Bibliography
gains oa_status and abstract_provider, provider takes the MetadataSource
vocabulary, and open_access is dropped. License, LicenseCode,
LicenseBasis, MetadataSource and OaStatus live in a new storage module
the parse imports, and storage Study gains license and oa_status, copied
(map entries, n/a priorities). The extraction record gets add_slot
deviations for license, oa_status and the publication_status #40 added,
so pondie can carry them.
study_schema.licensing.permits_redistribution reads the LicenseCode
`redistributable` and CopyAccess `redistribution` annotations: the text
follows text_copy, the abstract follows the paper licence.
Paper-parse 0.7.0 -> 0.8.0 (breaking), storage 0.8.0 -> 0.9.0,
extraction 0.5.0 -> 0.6.0. Models and JSON Schemas regenerated;
examples/licenses holds a TDM manuscript of a CC BY-NC article
(PMC10518222), a preprint-only paper, an assumed all-rights-reserved
paper and six refused shapes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
permits_redistribution now answers False for anything it does not recognise as permitted: no licence, an unknown code or access, or a redistributable code that is not stated by a named source in a non-empty statement. Its docs say the text call must pass the copy's access, and that counting NC and ND as redistributable, and `other` (Elsevier's OA user licence) as not, is policy rather than legal advice. stated_as and Bibliography.abstract take pattern \S: a missing abstract or statement is null, never "". Parse schema stays 0.8.0 (unreleased). New tests: a parse without Bibliography.license is refused; a copy off a preprint server that names one is refused (new invalid example); empty statements and abstracts are refused; malformed input is never permitted; the record's License has storage's fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
permits_redistribution(license, access=None) could not tell a held text checked without its access from an abstract check, so a forgotten access passed a TDM copy. It is replaced by text_may_be_redistributed(held_copy), which takes the HeldCopy so its access is always weighed, and abstract_may_be_redistributed(paper_license). Both keep the fail-closed rules; a held copy without an access or licence is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jdkent
left a comment
There was a problem hiding this comment.
code and basis are not descriptive enough names, and I have some questions, suggestions, perhaps a larger refactor as parsedpaper starts to share more with the studyschema itself, should they be merged into one object?
| Study.license: | ||
| transform: copy | ||
| source_artifact: neuroimaging-paper-parse/ParsedPaper | ||
| source_field: Bibliography.license |
There was a problem hiding this comment.
Where else would license come from? I don't think this field is necessary
There was a problem hiding this comment.
(Claude) Agreed. This entry only exists because storage Study carries its own license. If the licence stays on the bibliography (your next comment), both this map entry and the storage copy go.
🤖 Generated with Claude Code
There was a problem hiding this comment.
(Claude) Done in 2eff0d9. Study.license and this entry are gone. So are Study.oa_status and the four License.* entries. The map now has two whole-object copies, Study.bibliography ← ParsedPaper.bibliography and Study.identifiers ← ArtifactHeader.identifiers, in place of the 14 field copies.
🤖 Generated with Claude Code
| in_subset: [mvp, deterministic] | ||
| range: License | ||
| inlined: true | ||
| description: >- |
There was a problem hiding this comment.
License shouldn't have to be copied, if it belongs in bibliography then it should stay in the bib
There was a problem hiding this comment.
Should parsed paper be a strict subset of the study schema? Allowing these two representations to exist will lead to having to maintain logic to translate them
There was a problem hiding this comment.
(Claude) Agreed: one licence, on the bibliography. The copy came from storage Study having no bibliography of its own. How storage refers to it instead depends on your broader question in the next thread; I'll answer that there and fix both together.
🤖 Generated with Claude Code
There was a problem hiding this comment.
(Claude) Today they are two schemas on purpose: the parse is what was read from one paper, storage is the curated study after resolution and merging. extraction-to-storage.map.yaml joins them, and every shared field needs a copy rule there, which is the maintenance cost you describe. Options:
- Parse as a strict subset: storage imports the parse's shared classes (
Bibliography,ArticleIdentifiers,Copy) instead of redefining them, and keeps only storage-only fields of its own. The shared parts then need no translation. - One merged object: a parse is a partially filled storage
Study. - Status quo.
I'm writing these up with what each costs in study_schema, pondie, ingestion and neurostore, and will link it here before changing #41. The naming question (code, basis) goes in the same write-up.
🤖 Generated with Claude Code
There was a problem hiding this comment.
(Claude) Done in 2eff0d9. The licence now lives only in Bibliography.license, plus each copy's own Copy.license. Storage reads it as Study.bibliography.license: the same class, held whole, with no copy rule. Study.license, Study.oa_status, their priorities and the three extraction add_slots plus add_class License are removed.
🤖 Generated with Claude Code
There was a problem hiding this comment.
(Claude) Done in 2eff0d9, as option 1 of the write-up you approved.
Bibliography,ArticleIdentifiers,PreprintDoiandCorrection(and their vocabularies) are now defined once, in a new storage moduleneuroimaging-study-storage/bibliography.yaml. The parse imports it.- Storage
Studydrops its flat title, authors, journal, doi, publication_status, publication_year, language and study_type. In their place it holdsbibliographyandidentifiersunchanged, so nothing is translated. - The two fields that had drifted are reconciled in the shared class:
publication_typesisany_of [StudyType, string], andpublication_yearhas a minimum of 1600. reported_copiesmoved ontoParsedPaper, so copies stay parse-only.neurostore_base_study_idmoved to the header.- The extraction record carries no bibliographic fact any more, so pondie Drop the per-point sign from the coordinate parse; derive it from the values #21 deletes its PubMed fill.
- The two
test_identifiers.pystorage tests moved ontoStudy.identifierswith the same assertions.
A fully merged object (option 2) was not done, because the parse is mostly provenance and history that storage should not hold.
On naming: code is renamed license_id (SPDX's licenseId; the values are OpenAlex's licence slugs), and basis is renamed determination (stated / assumed).
🤖 Generated with Claude Code
|
|
||
| Two questions, one function each, so that neither can be asked with the other's facts: | ||
|
|
||
| - The parsed text: ``text_may_be_redistributed(paper.text_copy)``. It takes the held copy, |
There was a problem hiding this comment.
Does the text license and the abstract license differ in practice? Is this a meaningful separation?
There was a problem hiding this comment.
(Claude) Yes, for a large share, because a licence belongs to a copy, not to the paper.
- In a sample of 389 PMC copies in our catalog, 275 were author manuscripts licensed for text mining only, while the published version is often CC BY or CC BY-NC. For example, PMC10518222 is a mining-only manuscript of a CC BY-NC article: its abstract may be redistributed, its full text may not.
- Likewise, about 47k Elsevier and 10k Wiley texts were fetched with text-mining keys. Even when the article is open access, the bytes we hold came under a mining-only grant.
So the abstract follows the paper's licence and the text follows the copy we hold, and in practice they differ often.
🤖 Generated with Claude Code
There was a problem hiding this comment.
(Claude) Kept as two questions, now with signatures that can't be mixed up: text_may_be_redistributed(text_copy) takes the held copy, so its access is always weighed, and abstract_may_be_redistributed(bibliography.license) takes the paper's licence. Both refuse anything malformed. A CC BY copy obtained under a TDM grant is refused for the text, while its licence still permits the abstract (tested).
🤖 Generated with Claude Code
|
(Claude) On naming: agreed, 🤖 Generated with Claude Code |
|
(Claude) A write-up of the parse-versus-storage options, as promised in the study.yaml thread. Nothing in this PR changes until you pick one. What the schemas share today. Only bibliographic data. No class is defined in both schemas, and about 10
No consumer builds those storage fields from the map. neurostore reads the parse directly, and pondie fetches language and study type from PubMed itself. Recommendation: option 1, the parse as a strict subset of the study schema.
The other options:
How this PR would change under option 1:
It all rides this PR's single breaking release and the one planned re-upload. Names:
The values stay OpenAlex-style slugs such as One change to existing tests: two tests on main ( Shall I re-scope this PR to option 1? 🤖 Generated with Claude Code |
Bibliography, ArticleIdentifiers, PreprintDoi and Correction, with PublicationStatus, PreprintServer, CorrectionKind and StudyType, move to a new storage module, neuroimaging-study-storage/bibliography.yaml. The paper-parse imports it, so the parsed paper and the storage Study hold the same classes. Storage Study drops title, authors, journal, doi, publication_status, publication_year, language, study_type, license and oa_status for bibliography (required) and identifiers, each the parse's object unchanged. The map's 14 copy entries become 2 whole-object copies; the two that had drifted are reconciled in the shared class (publication_types takes any_of [StudyType, string]; publication_year keeps minimum 1600). The extraction record carries no bibliographic fact: the language, study_type, publication_status, license and oa_status add_slots and the License add_class are deleted. reported_copies moves to ParsedPaper beside text_copy, so copies stay parse-only, and neurostore_base_study_id moves to ArtifactHeader. License.code becomes license_id (LicenseId) and basis becomes determination (LicenseDetermination, stated | assumed). The two storage publication_status tests in test_identifiers.py move onto Study.identifiers with their assertions unchanged. One breaking release: paper-parse 0.8.0, storage 0.9.0, extraction 0.6.0, map 0.7.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vocabulary and field comparisons of generated models, which could only fail if the generator stopped inlining imports, are replaced: each shared class and enum is defined in exactly one storage YAML module, the parse imports the bibliography module, storage imports nothing from the parse, and every map copy names a field that exists, holding the same class, on both sides. A publication type outside StudyType is kept verbatim in the parse and in storage, and the field says StudyType lists known types only. license.yaml no longer speaks of a record copy; schema-tutorial.md's Study table, class inventory, sizes and module list describe the shared bibliography; README says language and publication-type criteria are answered by joining the record to its parse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Roadmap item
study-schema-license. It implements the licence contract (license-contract-design.md, with the user decisions of 2026-10-16). Since 2eff0d9 it is re-scoped to option 1 ofparse-storage-unification.md, which the user approved after reviewing this PR: the parse and storage share one bibliography, defined once. Built on main 6464de7, which includes #40.What changes
A licence belongs to a copy. A copy has four facts:
route:pmc,publisher,repository,preprint_serverorunrecorded.version:vor,amorsubmitted.license.access, for a copy we hold only:public,tdm_grantorunrecorded.The copies:
Copyis abstract, withHeldCopy(addsaccess) andReportedCopy(addsreported_by) beneath it. All three are parse-only.ParsedPaper.text_copyis required. It is the copy the text was read from.ParsedPaper.reported_copieslists the copies a source names that we don't hold.publisher-tdmis no longer a licence id. A text-mining grant isaccess: tdm_grant, recorded beside the copy's own licence.License, renamed after review. It has four fields:license_id(LicenseId). The values are OpenAlex's licence slugs:cc-by,cc-by-ncand so on, plusall-rights-reservedandother. SPDX names its licence identifierlicenseId, but the values here are not SPDX ids, which need a version that sources rarely state.determination(LicenseDetermination):statedorassumed. This is SPDX's declared/concluded split written as a field.stated_by(MetadataSource).stated_as.It lives in
neuroimaging-study-storage/license.yaml, along withMetadataSourceandOaStatus.One bibliography.
neuroimaging-study-storage/bibliography.yaml, holdsBibliography,ArticleIdentifiers,PreprintDoiandCorrection. The vocabulariesPublicationStatus,PreprintServer,CorrectionKindandStudyTypemove there with them.paper.yamlandheader.yamlimport this module, the same way they already import storage enums. Storage never imports the parse.Bibliography.licenseis the requiredLicenseof the version of record, or of the preprint whilepublication_statusispreprint.Bibliographygainsoa_statusandabstract_provider, and dropsopen_access.providertakes theMetadataSourcevocabulary.neurostore_base_study_idmoves fromArticleIdentifierstoArtifactHeader. It is the upload pointer, not an identifier of the paper.[mvp, deterministic]. The two copies that had drifted are reconciled:publication_typestakesany_of [StudyType, string];publication_yeartakesminimum_value: 1600.Storage
Studyno longer hastitle,authors,journal,doi,publication_status,publication_year,languageorstudy_type, and the earlierlicenseandoa_statusof this PR are gone too. Instead it gains:bibliography: Bibliography, which is required and replaces thetitlerequirement;identifiers: ArticleIdentifiers.Both are the parse's objects, unchanged. The licence is therefore
Study.bibliography.license, the same class with no copy rule.Storage no longer requires a publication type.
study_typewas required;Bibliography.publication_typesis optional, as it always was in the parse. That lets a preprint-only paper, which has no PubMed types, be stored.Nothing builds a storage
Studytoday. pondie imports no storage model, and neurostore's ingester reads theParsedPaperdicts directly (_bibliography,_set_retraction). The two map entries and the requiredbibliographyare the contract for a mapper when one is written.Language and publication types now come only from the parsed paper. Ingestion's
paper_parse._bibliographywrites them from PubMed; pondie's PubMed fill is deleted; neurostore persists neither. A criterion on either (English only, no reviews; 6 of the benchmark's 16 meta-analyses state one) is answered by joining the record to its parse throughparse_id. If neurostore must query them, that belongs on the neurostore item.StudyTypelists known types only; any other PubMed type is kept verbatim (tested).The map. The 14 copy entries become 2 whole-object identity copies:
Study.bibliography ← ParsedPaper.bibliographyandStudy.identifiers ← ArtifactHeader.identifiers. TheLicense.*,Study.licenseandStudy.oa_statusentries are deleted, as are the 8 flat bibliographic entries. Map version is 0.7.0.The extraction record carries no bibliographic fact. I deleted:
languageandstudy_typeadd_slots;publication_status,licenseandoa_statusadd_slots;add_class License.The
neuroimaging-study-extraction/study.yamlchange matches pondie's generator output (pondie #21 at 3da9c43) semantically. All 10 module YAMLs agree with it once parsed. The root YAML differs only in the existing description gap.Priorities.
Study.bibliographyandStudy.identifiersaren/a, and so are the fields of the shared classesBibliography,ArticleIdentifiers,PreprintDoi,CorrectionandLicense.Redistribution is derived, not stored.
study_schema.licensinghas one function per question:text_may_be_redistributed(text_copy). It takes theHeldCopy, so the access is always weighed with the licence.abstract_may_be_redistributed(bibliography.license).Text and abstract licences differ for about 275 of 389 sampled PMC copies, so they stay two questions.
Both functions read the
LicenseId.redistributableandCopyAccess.redistributionannotations, and both fail closed: they return False and never raise. That covers:statedby a named source in a non-empty statement.Two policy choices are built in. Counting every CC licence as redistributable, NC and ND included, is this project's policy reading, not legal advice.
other, which holds Elsevier's open-access user licence, is refused on purpose.Empty strings.
stated_asandabstracttakepattern: '\S', so a missing abstract or statement is null, never"".Rules (enforced by the JSON Schema, and null-safe):
The pydantic models don't evaluate LinkML rules. Ingestion's writer and neurostore's ingester must validate parses against
study_schema.jsonschemaat their boundaries.Release shape
One breaking release:
languageandstudy_typeare removed, and the record forbids unknown fields);The package version is unchanged, as with every earlier bump. Models and JSON Schemas are regenerated with
tools/generate_models.py.Old parses can't be read. A 0.7.0 parsed_paper fails under 0.8.0 because
text_copyandBibliography.licenseare now required, and nothing migrates it. Old storage Studies and records withlanguage/study_typefail too. Release order:download-copy.license-rerun), which also stripslanguage/study_typefrom the 1,817 run records outside git.Existing tests that changed
These two tests in
tests/test_identifiers.pyencoded the replaced flat shape:test_storage_study_carries_the_publication_status;test_storage_refuses_an_unknown_publication_status.They now build a
Studywithbibliographyandidentifiers, and assert onStudy.identifiers.publication_status, with the same assertions. The user's approval of option 1 covers this move. No other existing test was edited.Some example files also changed:
x7Ex4mpleA01: the header pointer,reported_copies, the renamed licence keys.examples/identifiers: the pointer key is dropped.examples/licenses/*: the new shape.Tests
In a fresh beast venv (
.[test,generate,layouts,parquet] jsonschema setuptools, Python 3.12),generate --checkis clean and 211 tests pass at 4e5c0e2 (209 at 2eff0d9). Main gives 149 passed and 1 skipped (the wheel test, which needs setuptools).pondie's
extraction_to_storage_map,storage_parameter_prioritiesandfield_provenancechecks (PR Drop the per-point sign from the coordinate parse; derive it from the values #21 head) report nothing new compared with main.Study.languagemissing-priority gap is gone.After the re-review, the generated-model comparisons (which could only fail if the generator stopped inlining imports) are replaced by checks on the YAML:
bibliography, and storage imports nothing from the parse;copynames a source and target field that exist and hold the same class.An unlisted publication type is accepted in the parse and in storage. Each new test failed under its mutation:
publication_typesclosed toStudyType; a mapsource_fieldrenamed;OaStatusredefined in the parse.New in this re-scope:
Study.bibliographyandStudy.identifiersvalidate the parse's dumps unchanged, in both pydantic and JSON Schema;bibliography;neurostore_base_study_idsits on the header and is refused inidentifiers.Every one of these mutations makes at least one test fail:
Study.bibliographymade optional;Bibliography.licensemade optional;identifiers;reported_copiesmoved back ontoBibliography;Study.language;licensingreadingbasisorcode;stated_aspattern removed.The earlier mutations (redistributable set, access annotation, each rule half,
text_copyrequired,publisher-tdm) are still covered by the same tests.What the consumers must change
pondie #21 (pin bump; replaces the separate
pondie-licenseitem, which has nothing left to copy):pondie/extraction/pubmed.py,_fill_from_pubmed(stages.py:685), its option (extraction/models.py:257) and the mention inrecord/fix/shape.py:412. The record no longer carrieslanguageorstudy_type.tests/fixtures/payloads/2abntY3hQSyq,tests/fixtures/coordinate_parse/gx2zzUbbydf9), and delete or reworktests/test_pubmed_study_type.pyandtests/test_record_formatting.py.Study, so nothing there readsStudy.bibliography. A future mapper would build it from the parse asstorage.Bibliography.model_validate(parse_bib.model_dump()).Ingestion:
download-copy: addsummary.copy(route, version, access, url) on each download row from oneDOWNLOAD_COPYregistry. It replacesarticle_version. Addurl_attempts[].license, and backfill with a backup.Licensetakeslicense_idanddetermination, and has noversion.publisher-tdm;_TDMmatches becomeaccess: tdm_grant.other.LicenseSourcebecomesMetadataSource, includingpmc_cloud.abstract_provider, andoa_statusinstead ofopen_access."", for a missing abstract.licenseto ourlicense_id.text_copyandreported_copiesgo onParsedPaper, with ano_copy_recordexit reason._headerwritesneurostore_base_study_id, and_identifiersstops writing it.License.Neurostore (after #1840 and the preprint base-study PR):
_resolve_base_studyreadsheader.neurostore_base_study_id._bibliographyreads the same fields, now typed by the shared model.BaseStudygainslicense(JSONB,bibliography.licensedumped whole),oa_statusandabstract_provider, through a migration, set on every upload.license_id,stated_asand the determination ("stated by Crossref" / "assumed: nobody stated a licence").text_may_be_redistributed(text_copy)and abstracts withabstract_may_be_redistributed(bibliography.license). Whether to hide the refused abstracts is still undecided (see the note in #1845).Open questions
None. Two were settled in review:
🤖 Generated with Claude Code