linkml-reference-validator 0.3.1rc1__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/PKG-INFO +23 -1
  2. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/README.md +22 -0
  3. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/concepts/content-types.md +1 -0
  4. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/concepts/how-it-works.md +64 -1
  5. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/fetch-full-text-and-pdfs.md +116 -8
  6. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/repair-validation-errors.md +68 -3
  7. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/use-local-files-and-urls.md +85 -3
  8. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/validate-titles.md +17 -0
  9. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/validate-urls.md +16 -1
  10. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/reference/cache-format.md +16 -1
  11. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/reference/cli.md +38 -3
  12. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/troubleshooting.md +93 -3
  13. linkml_reference_validator-0.5.0/skills/linkml-reference-validator/SKILL.md +67 -0
  14. linkml_reference_validator-0.5.0/skills/linkml-reference-validator/references/configuration.md +99 -0
  15. linkml_reference_validator-0.5.0/skills/linkml-reference-validator/references/guardrails.md +102 -0
  16. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/cache.py +105 -3
  17. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/cli/cache.py +59 -1
  18. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/cli/lookup.py +7 -18
  19. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/cli/repair.py +144 -38
  20. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/acquire.py +8 -0
  21. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/extract/__init__.py +3 -1
  22. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/extract/drawio.py +341 -0
  23. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/extract/html.py +37 -40
  24. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/extract/pdf.py +232 -0
  25. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/extract/svg.py +531 -55
  26. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/extract/svg_geometry.py +407 -0
  27. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/extract/untrusted_xml.py +57 -0
  28. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/extract/xml.py +470 -0
  29. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/__init__.py +11 -5
  30. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/base.py +20 -8
  31. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/fulltext/bioc.py +161 -0
  32. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/fulltext/bookshelf.py +242 -0
  33. linkml_reference_validator-0.5.0/src/linkml_reference_validator/etl/fulltext/epmc.py +197 -0
  34. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/json_api.py +7 -1
  35. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/openalex.py +2 -1
  36. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/pmc.py +1 -10
  37. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/unpaywall.py +2 -1
  38. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/identifiers.py +36 -1
  39. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/reference_fetcher.py +427 -109
  40. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/rules.py +65 -0
  41. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/doi.py +25 -19
  42. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/file.py +45 -32
  43. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/json_api.py +3 -8
  44. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/pmid.py +45 -0
  45. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/url.py +42 -6
  46. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/utils.py +43 -0
  47. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/field_detection.py +80 -23
  48. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/matching.py +5 -0
  49. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/models.py +108 -12
  50. linkml_reference_validator-0.5.0/src/linkml_reference_validator/plugins/reference_validation_plugin.py +1019 -0
  51. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/validation/fuzzy_text_utils.py +104 -25
  52. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/validation/repairer.py +153 -26
  53. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/validation/supporting_text_validator.py +165 -22
  54. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMC_TEST002.md +1 -1
  55. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMID_TEST003.md +1 -1
  56. linkml_reference_validator-0.5.0/tests/fixtures/bookshelf/esummary_20301710.xml +48 -0
  57. linkml_reference_validator-0.5.0/tests/fixtures/bookshelf/gene_NBK1116/men1.nxml +347 -0
  58. linkml_reference_validator-0.5.0/tests/fixtures/bookshelf/pubmed_20301710.xml +4 -0
  59. linkml_reference_validator-0.5.0/tests/test_bookshelf_provider.py +457 -0
  60. linkml_reference_validator-0.5.0/tests/test_bot_challenge.py +227 -0
  61. linkml_reference_validator-0.5.0/tests/test_cache_filename_collisions.py +168 -0
  62. linkml_reference_validator-0.5.0/tests/test_cache_read_robustness.py +235 -0
  63. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cache_validation.py +12 -4
  64. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cli_cache_reference.py +17 -0
  65. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_doi_preprint.py +4 -4
  66. linkml_reference_validator-0.5.0/tests/test_doi_url_encoding.py +82 -0
  67. linkml_reference_validator-0.5.0/tests/test_drawio.py +327 -0
  68. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_e2e_integration.py +1 -1
  69. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_empty_snippets.py +2 -2
  70. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_extractors.py +64 -0
  71. linkml_reference_validator-0.5.0/tests/test_field_detection_qualifiers.py +207 -0
  72. linkml_reference_validator-0.5.0/tests/test_file_reference_cache.py +131 -0
  73. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_frontmatter_and_doi_case.py +3 -3
  74. linkml_reference_validator-0.5.0/tests/test_fulltext_providers.py +903 -0
  75. linkml_reference_validator-0.5.0/tests/test_greek_variants.py +54 -0
  76. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_html_fulltext_acceptance.py +5 -2
  77. linkml_reference_validator-0.5.0/tests/test_html_scope_text.py +206 -0
  78. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_jats_tables.py +15 -8
  79. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_json_api_source.py +6 -6
  80. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_ligature_normalization.py +6 -2
  81. linkml_reference_validator-0.5.0/tests/test_long_cache_filenames.py +115 -0
  82. linkml_reference_validator-0.5.0/tests/test_meaning_changes.py +137 -0
  83. linkml_reference_validator-0.5.0/tests/test_meaning_symbols.py +121 -0
  84. linkml_reference_validator-0.5.0/tests/test_multivalued_reference.py +248 -0
  85. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_no_cache_downgrade.py +7 -3
  86. linkml_reference_validator-0.5.0/tests/test_plugin_value_shapes.py +348 -0
  87. linkml_reference_validator-0.5.0/tests/test_publisher_typography.py +138 -0
  88. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_reference_fetcher.py +55 -9
  89. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_repair.py +221 -0
  90. linkml_reference_validator-0.5.0/tests/test_repair_data_output.py +315 -0
  91. linkml_reference_validator-0.5.0/tests/test_repair_thresholds.py +243 -0
  92. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_rules.py +1 -0
  93. linkml_reference_validator-0.5.0/tests/test_source_network_errors.py +323 -0
  94. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_sources.py +28 -1
  95. linkml_reference_validator-0.5.0/tests/test_svg_edges.py +596 -0
  96. linkml_reference_validator-0.5.0/tests/test_title_only_shapes.py +335 -0
  97. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_title_validation.py +1 -0
  98. linkml_reference_validator-0.5.0/tests/test_url_fragment_cache.py +257 -0
  99. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_validation_config.py +3 -1
  100. linkml_reference_validator-0.5.0/tests/test_word_boundaries.py +151 -0
  101. linkml_reference_validator-0.5.0/tests/test_xml_extractor_text.py +333 -0
  102. linkml_reference_validator-0.3.1rc1/src/linkml_reference_validator/etl/extract/pdf.py +0 -136
  103. linkml_reference_validator-0.3.1rc1/src/linkml_reference_validator/etl/extract/xml.py +0 -253
  104. linkml_reference_validator-0.3.1rc1/src/linkml_reference_validator/plugins/reference_validation_plugin.py +0 -642
  105. linkml_reference_validator-0.3.1rc1/tests/test_fulltext_providers.py +0 -356
  106. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.claude/.mcp.json +0 -0
  107. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.claude/settings.json +0 -0
  108. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.copier-answers.yml +0 -0
  109. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.devcontainer/devcontainer.json +0 -0
  110. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.devcontainer/post_create.sh +0 -0
  111. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.editorconfig +0 -0
  112. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/actions/claude-code-action/action.yml +0 -0
  113. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/actions/claude-issue-summarize-action/action.yml +0 -0
  114. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/actions/claude-issue-triage-action/action.yml +0 -0
  115. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/ai-controllers.json +0 -0
  116. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/copilot-instructions.md +0 -0
  117. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/copilot-setup-steps.yml +0 -0
  118. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/dependabot.yml +0 -0
  119. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/claude-code-review.yml +0 -0
  120. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/claude-issue-summarize.yml +0 -0
  121. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/claude-issue-triage.yml +0 -0
  122. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/claude-ontology-review.yml +0 -0
  123. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/claude.yml +0 -0
  124. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/deploy-docs.yaml +0 -0
  125. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/dragon-ai.yml +0 -0
  126. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/main.yaml +0 -0
  127. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.github/workflows/pypi-publish.yaml +0 -0
  128. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.gitignore +0 -0
  129. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.goosehints +0 -0
  130. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.pre-commit-config.yaml +0 -0
  131. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/.yamllint.yaml +0 -0
  132. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/AGENTS.md +0 -0
  133. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/BUG_GEO_ACCESSION_TO_UID.md +0 -0
  134. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/BUG_ZENODO_DOI_AND_URL_HANDLING.md +0 -0
  135. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/CLAUDE.md +0 -0
  136. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/CLI-ISSUES.md +0 -0
  137. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/CODE_OF_CONDUCT.md +0 -0
  138. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/CONTRIBUTING.md +0 -0
  139. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/FEATURE_REQUEST_REPAIR.md +0 -0
  140. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/LICENSE +0 -0
  141. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/REVIEW2.md +0 -0
  142. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/REVIEW3.md +0 -0
  143. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/ai.just +0 -0
  144. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/benchmarks/repeated_quotes.py +0 -0
  145. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/copilot-instructions.md +0 -0
  146. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/about.md +0 -0
  147. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/assets/zotero-find-full-text.png +0 -0
  148. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/concepts/editorial-conventions.md +0 -0
  149. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/elements/.gitkeep +0 -0
  150. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/add-reference-source.md +0 -0
  151. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/export-cache-to-zotero.md +0 -0
  152. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/skip-unsupported-references.md +0 -0
  153. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/validate-clinical-trials.md +0 -0
  154. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/validate-dois.md +0 -0
  155. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/validate-entrez.md +0 -0
  156. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/how-to/validate-obo-files.md +0 -0
  157. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/index.md +0 -0
  158. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/notebooks/01_getting_started.ipynb +0 -0
  159. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/notebooks/02_advanced_usage.ipynb +0 -0
  160. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/notebooks/03_python_api.ipynb +0 -0
  161. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/notebooks/04_obo_validation.ipynb +0 -0
  162. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/notebooks/05_geo_validation.ipynb +0 -0
  163. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/quickstart.md +0 -0
  164. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/reference/splitting.md +0 -0
  165. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/setup-guide.md +0 -0
  166. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/superpowers/plans/2026-06-12-modular-pdf-fulltext-fetching.md +0 -0
  167. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/superpowers/plans/2026-08-05-private-library-full-text.md +0 -0
  168. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/superpowers/specs/2026-06-12-modular-pdf-fulltext-fetching-design.md +0 -0
  169. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/superpowers/specs/2026-08-05-private-library-full-text-design.md +0 -0
  170. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/templates-linkml/README.md +0 -0
  171. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/todo.md +0 -0
  172. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/docs/tutorials/complete-workflow.md +0 -0
  173. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/examples/README.md +0 -0
  174. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/justfile +0 -0
  175. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/mkdocs.yml +0 -0
  176. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/mypy.ini +0 -0
  177. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/notebooks/01_getting_started.ipynb +0 -0
  178. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/notebooks/02_advanced_usage.ipynb +0 -0
  179. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/notebooks/03_python_api.ipynb +0 -0
  180. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/notebooks/README.md +0 -0
  181. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/project/README.md +0 -0
  182. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/project.justfile +0 -0
  183. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/pyproject.toml +0 -0
  184. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/pytest.ini +0 -0
  185. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/__init__.py +0 -0
  186. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/_version.py +0 -0
  187. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/cli/__init__.py +0 -0
  188. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/cli/shared.py +0 -0
  189. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/cli/validate.py +0 -0
  190. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/__init__.py +0 -0
  191. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/csl_export.py +0 -0
  192. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/extract/base.py +0 -0
  193. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/epmc_preprint.py +0 -0
  194. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/loader.py +0 -0
  195. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/fulltext/zotero.py +0 -0
  196. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/__init__.py +0 -0
  197. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/base.py +0 -0
  198. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/clinicaltrials.py +0 -0
  199. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/entrez.py +0 -0
  200. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/loader.py +0 -0
  201. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/sources/ppr.py +0 -0
  202. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/etl/text_extractor.py +0 -0
  203. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/plugins/REVIEW.md +0 -0
  204. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/plugins/__init__.py +0 -0
  205. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/src/linkml_reference_validator/validation/__init__.py +0 -0
  206. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/__init__.py +0 -0
  207. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/conftest.py +0 -0
  208. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/data/test_data_deep_nesting_invalid.yaml +0 -0
  209. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/data/test_data_deep_nesting_valid.yaml +0 -0
  210. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/data/test_data_invalid.yaml +0 -0
  211. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/data/test_data_valid.yaml +0 -0
  212. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/data/test_schema.yaml +0 -0
  213. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/data/test_schema_deep_nesting.yaml +0 -0
  214. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/GEO_GSE12345.md +0 -0
  215. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMC_TEST001.md +0 -0
  216. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMC_TEST001.txt +0 -0
  217. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMC_TEST002.txt +0 -0
  218. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMID_TEST001.md +0 -0
  219. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMID_TEST001.txt +0 -0
  220. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMID_TEST002.md +0 -0
  221. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMID_TEST002.txt +0 -0
  222. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/PMID_TEST003.txt +0 -0
  223. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/clinicaltrials_NCT00000001.md +0 -0
  224. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/fulltext_html/README.md +0 -0
  225. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/fulltext_html/dspace-32894695.html +0 -0
  226. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/fulltext_html/iris-19049553.html +0 -0
  227. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/fulltext_html/plos-0000308.html +0 -0
  228. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/jats/PMC5593426.xml +0 -0
  229. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/jats/README.md +0 -0
  230. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/fixtures/sample.obo +0 -0
  231. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_absent_abstract_recovery.py +0 -0
  232. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_acquire.py +0 -0
  233. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cache_author_recovery.py +0 -0
  234. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cli.py +0 -0
  235. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cli_cache_enrich.py +0 -0
  236. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cli_cache_export.py +0 -0
  237. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cli_lookup.py +0 -0
  238. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_cli_repair.py +0 -0
  239. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_clinicaltrials_cache.py +0 -0
  240. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_clinicaltrials_integration.py +0 -0
  241. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_deep_nesting.py +0 -0
  242. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_epmc_preprint_provider.py +0 -0
  243. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_extractor_fidelity.py +0 -0
  244. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_field_detection.py +0 -0
  245. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_fulltext_loader.py +0 -0
  246. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_fuzzy_suggestions.py +0 -0
  247. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_geo_integration.py +0 -0
  248. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_identifiers.py +0 -0
  249. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_models.py +0 -0
  250. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_normalized_content_cache.py +0 -0
  251. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_open_access_policy.py +0 -0
  252. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_optional_linkml_dependency.py +0 -0
  253. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_pdf_url_title.py +0 -0
  254. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_plugin_integration.py +0 -0
  255. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_pmc_fulltext.py +0 -0
  256. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_pmid_network.py +0 -0
  257. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_ppr_source.py +0 -0
  258. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_real_validation.py +0 -0
  259. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_serve_stale_html_optin.py +0 -0
  260. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_source_utils.py +0 -0
  261. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_split_supporting_text.py +0 -0
  262. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_supporting_text_validator.py +0 -0
  263. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_svg_extractor.py +0 -0
  264. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_text_extractor.py +0 -0
  265. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_trust_cached_entries.py +0 -0
  266. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_unknown_slot_traversal.py +0 -0
  267. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_url_html_sanitize.py +0 -0
  268. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_url_html_to_text.py +0 -0
  269. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_url_source_cache_version.py +0 -0
  270. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_validation_counts.py +0 -0
  271. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/tests/test_zotero_provider.py +0 -0
  272. {linkml_reference_validator-0.3.1rc1 → linkml_reference_validator-0.5.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: linkml_reference_validator
3
- Version: 0.3.1rc1
3
+ Version: 0.5.0
4
4
  Summary: Validation of supporting text from references and publications
5
5
  Author-email: Chris Mungall <cjmungall@lbl.gov>
6
6
  License-Expression: Apache-2.0
@@ -58,6 +58,28 @@ linkml-validate -s schema.yaml data.yaml \
58
58
 
59
59
  ---
60
60
 
61
+ ## Agent skill
62
+
63
+ Install the [linkml-reference-validator workflow skill](skills/linkml-reference-validator/SKILL.md) with the
64
+ [skills CLI](https://skills.sh/) (requires Node.js). Preview available skills first:
65
+
66
+ ```bash
67
+ npx skills add linkml/linkml-reference-validator --list
68
+ npx skills add linkml/linkml-reference-validator --skill linkml-reference-validator
69
+ ```
70
+
71
+ Installation defaults to the current project. Use `-a codex` or
72
+ `-a claude-code` to select an agent; add `-g` for your user-wide skills directory:
73
+
74
+ ```bash
75
+ npx skills add linkml/linkml-reference-validator --skill linkml-reference-validator -a codex -g
76
+ ```
77
+
78
+ The skill helps agents configure reference QC, interpret failures and coverage,
79
+ and set up repository hooks and CI using bundled reference guides. Install the
80
+ runtime separately as described above; installing the skill does not activate
81
+ hooks or configure CI, credentials, or data sources.
82
+
61
83
  ## Why Use This Tool?
62
84
 
63
85
  Scientific data often includes claims supported by quotes from publications. But how do you know the quotes are accurate?
@@ -37,6 +37,28 @@ linkml-validate -s schema.yaml data.yaml \
37
37
 
38
38
  ---
39
39
 
40
+ ## Agent skill
41
+
42
+ Install the [linkml-reference-validator workflow skill](skills/linkml-reference-validator/SKILL.md) with the
43
+ [skills CLI](https://skills.sh/) (requires Node.js). Preview available skills first:
44
+
45
+ ```bash
46
+ npx skills add linkml/linkml-reference-validator --list
47
+ npx skills add linkml/linkml-reference-validator --skill linkml-reference-validator
48
+ ```
49
+
50
+ Installation defaults to the current project. Use `-a codex` or
51
+ `-a claude-code` to select an agent; add `-g` for your user-wide skills directory:
52
+
53
+ ```bash
54
+ npx skills add linkml/linkml-reference-validator --skill linkml-reference-validator -a codex -g
55
+ ```
56
+
57
+ The skill helps agents configure reference QC, interpret failures and coverage,
58
+ and set up repository hooks and CI using bundled reference guides. Install the
59
+ runtime separately as described above; installing the skill does not activate
60
+ hooks or configure CI, credentials, or data sources.
61
+
40
62
  ## Why Use This Tool?
41
63
 
42
64
  Scientific data often includes claims supported by quotes from publications. But how do you know the quotes are accurate?
@@ -12,6 +12,7 @@ When fetching references, the validator tracks what type of content was retrieve
12
12
  | `summary` | GEO, BioProject, BioSample, ClinicalTrials | Database summary/description field |
13
13
  | `local_file` | file: | Content read from local filesystem |
14
14
  | `local_svg` | file: (`.svg`) | Text an SVG figure renders, a blank line between labels; a quote must fall within one label |
15
+ | `local_drawio` | file: (`.drawio`) | Labels a drawio diagram draws, read like `local_svg` |
15
16
  | `url` | url: | Content fetched from web URL |
16
17
  | `unavailable` | Any | No content could be retrieved |
17
18
 
@@ -13,7 +13,23 @@ linkml-reference-validator validates that quoted text (supporting text) actually
13
13
  Before matching, both the supporting text and reference content are normalized:
14
14
 
15
15
  - **Lowercased**: `"MUC1"` → `"muc1"`
16
- - **Punctuation removed**: `"c-Abl"` → `"c abl"`
16
+ - **Punctuation removed**: `"c-Abl"` → `"c abl"` (a point or comma between
17
+ digits is kept, so `"10.5"` stays one number)
18
+ - **Meaning-bearing symbols kept as words**: `<` `>` `≤` `≥` become `lt` `gt`
19
+ `le` `ge`, `±` (or `+/-`) becomes `pm`, and a minus sign before a number stays
20
+ on it, so `"p < 0.05"` does not match `"p > 0.05"` and `"2.5 fold"` does not
21
+ match `"-2.5 fold"`. ASCII and Unicode spellings are equivalent (`<=` and `≤`,
22
+ `-` and `−`). Publisher spellings count too: MEDLINE's `>/=` and `</=` for
23
+ `≥` and `≤`, a middle-dot decimal (`57·4` is `57.4`), and an en dash or
24
+ Unicode hyphen printed as a minus sign (`z score, –1.98`) or inside `+/–`.
25
+ A `−` set off from its number by a space (`from − 2.92`) is still a sign,
26
+ unless a number comes before it (`10 − 2` is a subtraction). A hyphen or en
27
+ dash after a word or digit (`IL-6`, `3-5`, `1–12`) is still punctuation. A
28
+ dash after a space and before a digit is a sign, so a range spaced on one
29
+ side only (`18 –65`) reads as `18` and `-65`; quote it as the source
30
+ prints it. A middle dot after a letter-joined number is not a decimal
31
+ point, so `MgCl2·6H2O` keeps its hydrate dot (and `pH7·4` does not read as
32
+ `pH7.4`; `pH 7·4` does).
17
33
  - **Whitespace collapsed**: Multiple spaces become single space
18
34
  - **Editorial notes removed**: `"[mucin 1]"` → `""`
19
35
 
@@ -36,6 +52,34 @@ reference_content = "...The MUC1 oncoprotein blocks nuclear..."
36
52
  # Match: "muc1 oncoprotein" found in normalized reference
37
53
  ```
38
54
 
55
+ **Matches start and end on whole words:**
56
+
57
+ A quote may not begin or end inside a word of the reference. Otherwise a quote
58
+ stating the opposite of the paper would pass:
59
+
60
+ ```python
61
+ supporting_text = "activates AKT signaling"
62
+ reference_content = "...Loss of PTEN inactivates AKT signaling..."
63
+ # NOT FOUND: "activates" only occurs inside "inactivates"
64
+
65
+ supporting_text = "5 mM H2O2"
66
+ reference_content = "...treated with 10.5 mM H2O2..."
67
+ # NOT FOUND: "5" only occurs inside "10.5"
68
+ ```
69
+
70
+ Each part of a quote split with `...` is held to the same rule.
71
+
72
+ One allowance: when a quote ends in a letter, the reference word it ends on may
73
+ carry trailing digits. Full-text extraction welds superscript citations onto the
74
+ word before them, so `tumor suppressor<sup>12</sup>` reads as `suppressor12`,
75
+ and a quote of `PTEN is a tumor suppressor` must still match. A quote ending in
76
+ a digit gets no allowance (`10` never matches `105`). The same allowance means a
77
+ quote ending in `AKT` matches `AKT1`, as plain substring matching always did.
78
+
79
+ Because a point or comma between digits stays part of the number, numbers must
80
+ be quoted with the reference's own separators: `days 3, 5 and 7` does not match
81
+ `days 3,5 and 7`, and `10 000` does not match `10,000`.
82
+
39
83
  **Empty excerpts are rejected:**
40
84
 
41
85
  An empty or whitespace-only excerpt is always an error, never a pass. The
@@ -224,6 +268,16 @@ content_type: abstract_only
224
268
  The MUC1 oncoprotein blocks nuclear targeting...
225
269
  ```
226
270
 
271
+ An entry with no title has no `# title`, authors or journal lines, but it still
272
+ gets the `## Content` marker. The content is everything after the first exact
273
+ `## Content` line of that header, so a heading in the text such as
274
+ `## Contents of the kit` stays part of the content. A file written before untitled
275
+ entries got the marker is read whole.
276
+
277
+ A cache file whose frontmatter is empty, isn't valid YAML, or isn't a mapping is
278
+ treated as missing. Validation re-fetches the reference and rewrites the file,
279
+ and tools that walk the cache skip the file with a warning.
280
+
227
281
  ### Cache Benefits
228
282
 
229
283
  - **Offline usage**: Work without network after initial fetch
@@ -254,6 +308,15 @@ The plugin discovers fields via:
254
308
  - `slot_uri` attribute (e.g., `slot_uri: dcterms:title`)
255
309
  - Fallback slot names (`reference`, `supporting_text`, `title`)
256
310
 
311
+ Besides the URIs above, a URI of your own counts as a reference or excerpt
312
+ field when its local name *ends* in the word. One `Field`, `Slot` or `Value`
313
+ suffix may follow it, and for references also an identifier suffix such as
314
+ `Id` or `Uri`. So `ex:reference`, `ex:cross_reference`, `ex:myReferenceField`
315
+ and `ex:reference_id` are reference fields, and `ex:myExcerptField` is an
316
+ excerpt field. `ex:referenceType`, `ex:excerpt_location` and `ex:excerpt_id`
317
+ describe a reference or excerpt, so they are not. If a field of yours is missed
318
+ (say `ex:excerpt_text`), declare it with `implements` or one of the URIs above.
319
+
257
320
  ## Editorial Conventions
258
321
 
259
322
  ### Square Brackets `[...]`
@@ -21,17 +21,34 @@ The validator tries providers in order until one returns usable full text.
21
21
  The default order is:
22
22
 
23
23
  ```
24
- pmc → epmc_preprint → unpaywall → openalex
24
+ pmc → bookshelf → epmc_preprint → unpaywall → openalex
25
25
  ```
26
26
 
27
27
  - **`pmc`** — PubMed Central open-access subset (XML/HTML full text for many
28
28
  biomedical articles).
29
+ - **`bookshelf`** — NCBI Bookshelf chapters, such as GeneReviews, read from a
30
+ local copy of NCBI's archive (see [NCBI Bookshelf](#ncbi-bookshelf-genereviews)
31
+ below). Does nothing until `bookshelf_archive_dir` is set.
29
32
  - **`epmc_preprint`** — Europe PMC preprint body text via the `fulltextRepo`
30
33
  PDF route (see [Preprints](#preprints) below).
31
34
  - **`unpaywall`** — Unpaywall open-access lookup by DOI (often a publisher or
32
35
  repository PDF).
33
36
  - **`openalex`** — OpenAlex open-access location (PDF/HTML).
34
37
 
38
+ Two more built-in providers are available but not in the default chain; add
39
+ them to `full_text_providers` to use them:
40
+
41
+ - **`bioc`** — PMC Open Access articles as BioC passages, from NCBI's BioNLP
42
+ BioC API, by PMID. The reference list and the competing-interest,
43
+ author-contribution and funding statements are left out, so a snippet quoting
44
+ a cited paper cannot validate against the paper citing it.
45
+ - **`epmc`** — Europe PMC's `fullTextXML` for open-access articles, by PMID.
46
+ This is the same JATS that `pmc` reads, served by Europe PMC rather than
47
+ NCBI, so after `pmc` it rarely finds anything new. It is useful as a second
48
+ route when NCBI is down or rate-limiting, or in place of `pmc` to avoid
49
+ depending on Entrez. Not to be confused with `epmc_preprint`, which handles
50
+ preprints only.
51
+
35
52
  Providers are skipped silently if they have nothing for a given reference, so
36
53
  the chain degrades gracefully: a miss in `pmc` simply moves on to `unpaywall`,
37
54
  and so on. If no provider yields full text, the reference keeps whatever
@@ -45,7 +62,8 @@ your config YAML or on the `ReferenceValidationConfig` object):
45
62
  | Key | Default | Description |
46
63
  |-----|---------|-------------|
47
64
  | `fetch_full_text` | `true` | Attempt to obtain full text via the provider chain when a metadata source does not already return full text. |
48
- | `full_text_providers` | `[pmc, epmc_preprint, unpaywall, openalex]` | Ordered list of provider names to try until one yields usable full text. |
65
+ | `full_text_providers` | `[pmc, bookshelf, epmc_preprint, unpaywall, openalex]` | Ordered list of provider names to try until one yields usable full text. `bioc` and `epmc` are also built in but must be added explicitly. |
66
+ | `bookshelf_archive_dir` | `null` | Directory of unpacked NCBI Bookshelf books, read by the `bookshelf` provider. |
49
67
  | `private_cache_dir` | `~/.cache/linkml-reference-validator/private` | Separate research cache for closed/user-library full text; never read by validation. |
50
68
  | `pdf_backend` | `pypdf` | Name of the PDF text-extraction backend. |
51
69
  | `download_pdfs` | `true` | If true, persist downloaded PDFs to the files cache directory. |
@@ -66,6 +84,7 @@ email: you@example.org
66
84
  fetch_full_text: true
67
85
  full_text_providers:
68
86
  - pmc
87
+ - bookshelf
69
88
  - epmc_preprint
70
89
  - unpaywall
71
90
  - openalex
@@ -73,6 +92,91 @@ pdf_backend: pypdf
73
92
  download_pdfs: true
74
93
  ```
75
94
 
95
+ ## NCBI Bookshelf (GeneReviews)
96
+
97
+ A PMID can point to a chapter on NCBI Bookshelf rather than a journal article.
98
+ Every [GeneReviews](https://www.ncbi.nlm.nih.gov/books/NBK1116/) entry is one
99
+ of these. Such a chapter has no PMC copy, so without help the reference keeps
100
+ only its PubMed summary, and a quote from the chapter body fails as
101
+ abstract-only.
102
+
103
+ The archive described below is for **caching** a chapter, not for validating
104
+ against it. Validate quotes against the cached document. Once a chapter has
105
+ been fetched, its full text is in the reference cache (`PMID_31536184.md`),
106
+ and every later validation reads that file. There's no archive and no network
107
+ call. So the large download is a one-time step on the machine that builds the
108
+ cache, not something every run or every collaborator needs. See
109
+ [Validate against the cache, not the archive](#validate-against-the-cache-not-the-archive).
110
+
111
+ Bookshelf's web pages can't be fetched by a script: NCBI answers with a
112
+ reCAPTCHA page. Instead, the `bookshelf` provider reads chapters from NCBI's
113
+ [litarch archive](https://ftp.ncbi.nlm.nih.gov/pub/litarch/), which you
114
+ download once per book:
115
+
116
+ ```bash
117
+ linkml-reference-validator cache bookshelf-download NBK1116 --archive-dir bookshelf
118
+ ```
119
+
120
+ GeneReviews (`NBK1116`) is a 640 MB download. Only the chapter XML is kept,
121
+ about 200 MB. Then point the config at the same directory:
122
+
123
+ ```yaml
124
+ bookshelf_archive_dir: bookshelf
125
+ ```
126
+
127
+ The PubMed record names the chapter, so no extra lookup is needed:
128
+ `PMID:31536184` is `gene/glutaric-a1`, read from
129
+ `bookshelf/gene_NBK1116/glutaric-a1.nxml`. The cached entry records
130
+ `full_text_provider: bookshelf`, and `full_text_url` holds the chapter's
131
+ Bookshelf page.
132
+
133
+ A chapter is used only when its own `<license>` is `open-access`. GeneReviews
134
+ allows copying for noncommercial research with credit and no modification. A
135
+ cache holds a verbatim copy, and its `full_text_url` links back to the
136
+ chapter, so the text goes into the public cache like PMC full text does.
137
+
138
+ To refresh a book, run the download again: it replaces the book's folder. Do
139
+ this to pick up new or revised chapters.
140
+
141
+ **References cached before the archive was set up** keep their abstract. The
142
+ chain recorded that it couldn't read the chapter (`full_text_declined`, or
143
+ `full_text_attempted` on a cache from an older version), and it doesn't retry
144
+ on its own. Re-fetch them:
145
+
146
+ ```bash
147
+ linkml-reference-validator cache reference PMID:31536184 --force
148
+ ```
149
+
150
+ ### Validate against the cache, not the archive
151
+
152
+ The archive is only read when a chapter is **fetched**. That happens the first
153
+ time a reference is seen, and on an explicit `--force`. Validation reads the
154
+ cached document like any other cached reference. In practice:
155
+
156
+ 1. On one machine, download the book, set `bookshelf_archive_dir`, and cache
157
+ the chapters you cite (`cache reference PMID:...`, or a validation run).
158
+ 2. Commit the reference cache: the `PMID_*.md` files, which hold the chapter
159
+ text. Don't commit the archive.
160
+ 3. Everywhere else, including CI and collaborators, validate as usual. No
161
+ archive is needed, `bookshelf_archive_dir` can stay unset, and nothing is
162
+ downloaded from NCBI.
163
+
164
+ What happens when the archive is missing:
165
+
166
+ - **A cached chapter is never lost to a refresh that can't reach it.** If a
167
+ newer version of this tool marks the entry stale and re-fetches it, the
168
+ refresh gets only the abstract. The cached chapter is then kept and still
169
+ used, with a warning, and the entry stays stale until a run with the
170
+ archive refreshes it. For a committed cache you'd rather not re-fetch at
171
+ all, set `trust_cached_entries: true`.
172
+ - **`--force` is the exception.** It replaces the entry with whatever the
173
+ fetch returns, so forcing a Bookshelf reference without the archive swaps
174
+ the chapter for its abstract (it warns as it does). Only force these
175
+ references where the archive is available.
176
+ - **A reference that was never cached** still needs the archive once.
177
+ Without it, the reference is cached with its abstract only.
178
+ `full_text_declined: bookshelf_archive_not_configured` records why.
179
+
76
180
  ## Private manuscripts in Zotero
77
181
 
78
182
  The opt-in **`zotero`** provider can find non-open manuscripts that are already
@@ -270,7 +374,7 @@ the text came from. These provenance fields are written alongside the content:
270
374
 
271
375
  | Field | Description |
272
376
  |-------|-------------|
273
- | `full_text_provider` | Name of the provider that supplied the full text (e.g. `pmc`, `epmc_preprint`, `unpaywall`, or a custom name). |
377
+ | `full_text_provider` | Name of the provider that supplied the full text (e.g. `pmc`, `bioc`, `epmc`, `epmc_preprint`, `unpaywall`, or a custom name). |
274
378
  | `oa_status` | Open-access status reported by the provider (e.g. `gold`, `green`, `bronze`). |
275
379
  | `license` | License of the open-access copy, when known. |
276
380
  | `local_pdf_path` | Path (relative to the cache directory) to the downloaded PDF, when one was persisted. |
@@ -301,14 +405,18 @@ fetcher does not crawl repository download links. If every provider completes
301
405
  without usable text or an error, the reference retains its original abstract
302
406
  and records `full_text_attempted: true`.
303
407
 
304
- New `full_text_html` cache entries carry `html_full_text_version: 1`. Older
305
- Markdown HTML full-text entries without this stamp are refreshed on their next
306
- validation read. This migration covers all providers because cached plain text
408
+ New `full_text_html` cache entries carry `html_full_text_version: 2`. Entries
409
+ with an older stamp, or none, are refreshed on their next validation read.
410
+ Version 2 keeps all of an article region's text rather than only its `<p>`
411
+ text, so lists, tables, headings and bare `<div>` text can be quoted. This migration covers all providers because cached plain text
307
412
  cannot establish which HTML body structure was present. It does not invalidate
308
413
  current PDF, XML, or abstract caches.
309
414
 
310
- If refreshing an old HTML entry fails, validation does **not** fall back to that
311
- entry: its text may be repository metadata rather than article evidence. The
415
+ If refreshing an entry stamped `html_full_text_version: 1` or later fails,
416
+ validation falls back to it with a warning, as for other content types: it is a
417
+ checked article body, though it may lack list and table text. If refreshing an
418
+ entry with no stamp fails, validation does **not** fall back to that entry: its
419
+ text may be repository metadata rather than article evidence. The
312
420
  file remains untouched for inventory/export, and the next validation run retries
313
421
  recovery. Once the source is available, normal fetching replaces it with a
314
422
  verified article or the available metadata/abstract. Other content types retain
@@ -26,7 +26,25 @@ The distinct letters `Æ/æ` and `Œ/œ` remain distinct from `AE/ae` and `OE/oe
26
26
  Equating these letters would accept spelling changes beyond typographic repair.
27
27
  Validation uses an explicit ligature table instead of broad Unicode NFKC
28
28
  normalization so this change does not equate scientific notation such as `H₂O`
29
- with `H2O`, `10⁶` with `106`, or the micro sign `µ` with Greek `μ`.
29
+ with `H2O` or `10⁶` with `106`. The micro sign `µ` and Greek `μ` *are* treated
30
+ as the same letter, since sources use them interchangeably (`5 µM` matches
31
+ `5 μM`), as are the variant Greek forms `ϕ ϵ ϑ ϐ ϖ ϰ ϱ` and the ordinary
32
+ letters they stand for.
33
+
34
+ ### Unmapped PDF glyphs do not match
35
+
36
+ Some PDFs draw a glyph without saying which character it is, so text extraction
37
+ gets no character at all. This is common for `fi`/`fl` ligatures, and sometimes
38
+ happens for digits or minus signs. PDF extraction writes U+FFFD (`�`) in its
39
+ place and logs a warning with the count, so cached text reads `in�ammation`.
40
+ Validation does not guess the missing character: a quote containing
41
+ `inflammation` will not match that text. Check the cached file for `�` and
42
+ quote around the damaged word or supply better full text.
43
+
44
+ Matching treats `�` like punctuation, so the damaged word splits in two: the
45
+ cached text above reads as `in ammation`. A quote that starts or ends inside
46
+ the damaged word still matches, so `ammation of the gut` matches
47
+ `in�ammation of the gut`.
30
48
 
31
49
  ## Quick Start
32
50
 
@@ -72,6 +90,18 @@ The repair command uses confidence thresholds to determine how to handle each er
72
90
  | **LOW** | 0.50-0.80 | Flag for manual review |
73
91
  | **VERY_LOW** | 0.00-0.50 | Recommend removal |
74
92
 
93
+ The score ranges are the defaults. A fuzzy match is graded by the repair
94
+ config's `auto_fix_threshold`, `suggest_threshold` and `removal_threshold`, so
95
+ raising `auto_fix_threshold` (or passing `--auto-fix-threshold`) turns
96
+ borderline auto-fixes into suggestions, and lowering it does the reverse. The
97
+ three must be in order, `removal <= suggest <= auto_fix`. A lower threshold
98
+ you leave unset follows one you set below it (`auto_fix_threshold: 0.75` alone
99
+ also lowers `suggest_threshold` to 0.75), and `--auto-fix-threshold` lowers the
100
+ other two with it. Thresholds you set out of order are an error.
101
+ The flag overrides the config file only when it is given. Character
102
+ normalization and ellipsis repairs are not scored, so the thresholds do not
103
+ apply to them.
104
+
75
105
  ## Repair Strategies
76
106
 
77
107
  ### 1. Character Normalization (High Confidence)
@@ -90,11 +120,30 @@ supporting_text: "CO₂ levels were measured"
90
120
 
91
121
  Common mappings:
92
122
  - `CO2` → `CO₂`
123
+ - `H2O2` → `H₂O₂`
93
124
  - `H2O` → `H₂O`
94
125
  - `O2` → `O₂`
95
126
  - `+/-` → `±`
96
127
  - `+-` → `±`
97
128
 
129
+ Where a mapping starts or ends with a letter or digit, it applies only if no
130
+ letter or digit sits directly next to it on that side. So `O2` is left alone in
131
+ `CDKN2A` and `N2` in `HEK293N2`. The same rule also skips formulas joined to a
132
+ coefficient or prefix, such as `6CO2`, `2H2O2` and `pCO2`. Fix those by hand.
133
+ A mapping that starts and ends with punctuation, such as `+/-`, applies
134
+ anywhere, so `5+/-2` becomes `5±2`. All mappings are applied in one pass,
135
+ longest first.
136
+
137
+ Validation reads `+-` (except before a letter), `+/-` and `±` all as the same
138
+ word, ignores other punctuation, spells out Greek letters and folds ligatures,
139
+ so it cannot confirm a mapping that changes only those, such as `+-` → `±` or a
140
+ custom `alpha` → `α`. Such a mapping is kept only when the reference contains
141
+ the repaired text exactly as written, apart from case and whitespace.
142
+ That is what keeps `Ca2+-dependent` from becoming `Ca2±dependent`.
143
+
144
+ Quotes with `...` or `[editorial brackets]` are checked part by part, the same
145
+ way validation checks them.
146
+
98
147
  ### 2. Ellipsis Insertion (Medium Confidence)
99
148
 
100
149
  When text parts exist in the reference but aren't contiguous:
@@ -121,6 +170,22 @@ Suggested fix (85%): "Haemophilus influenzae type b" → "H. influenzae type b"
121
170
 
122
171
  Use with caution - verify the suggestion preserves the intended meaning.
123
172
 
173
+ A correction that would change what the quote claims is reported, but never
174
+ offered or applied as the fix:
175
+
176
+ - **It drops a word of the quote,** such as "no" or "in mice".
177
+ - **It swaps a word for one that differs only in a prefix,** such as
178
+ "unaffected" for "affected", "hyperphosphorylated" for
179
+ "hypophosphorylated", or "intracellular" for "extracellular". This holds
180
+ even when the quote also has a typo ("unaffectd").
181
+ - **It adds a negation** the quote doesn't have, such as "not", "never",
182
+ "without" or "failed to".
183
+
184
+ Words of three letters or fewer must match exactly, so "in" is never
185
+ corrected to "on". These checks err towards caution, so some real typos go
186
+ unfixed and need fixing by hand: a typo in a word's first few letters can look
187
+ like a changed prefix ("ctivation" reads as "activation" without its "a").
188
+
124
189
  ### 4. Removal Recommendation (Very Low Confidence)
125
190
 
126
191
  Flags text that appears fabricated or hallucinated:
@@ -214,8 +279,8 @@ linkml-reference-validator repair data <DATA_FILE> --schema <SCHEMA> [OPTIONS]
214
279
  - `--schema PATH` - Path to LinkML schema (required)
215
280
  - `--target-class CLASS` - Target class to validate
216
281
  - `--dry-run / --no-dry-run` - Show changes without applying (default: dry-run)
217
- - `--auto-fix-threshold FLOAT` - Minimum similarity for auto-fixes (default: 0.95)
218
- - `--output PATH` - Output file path (default: overwrite with backup)
282
+ - `--auto-fix-threshold FLOAT` - Minimum similarity for auto-fixes (default: the config file's `auto_fix_threshold`, else 0.95)
283
+ - `--output PATH` - Output file path (default: overwrite the input, keeping a `.bak` backup). When given with `--no-dry-run`, it is always written, as an unchanged copy of the input when there is nothing to fix, and the input itself is not touched, so no backup is made
219
284
  - `--config PATH` - Path to repair configuration file
220
285
  - `--cache-dir PATH` - Directory for caching references
221
286
  - `--verbose` - Show detailed output
@@ -141,14 +141,95 @@ linkml-reference-validator validate text \
141
141
  `-webkit-text-security`, a `unicode-bidi` override, `<bdo>`, or a bidi
142
142
  control character in the text.
143
143
 
144
+ **Edges.** A connector between two labelled nodes also gives an edge line,
145
+ `"<source> <edge label> <target>"`, so a relational quote such as
146
+ `"PTEN inhibits AKT"` can be checked. The connector is an element (usually a
147
+ `<g>` holding the line and its label) whose `data-source` and `data-target`
148
+ attributes name the two nodes' ids:
149
+
150
+ ```xml
151
+ <defs><marker id="arrow"><path d="M0 0 L10 5 L0 10 z"/></marker></defs>
152
+ <g id="pten"><rect x="0" y="0" width="60" height="30"/><text x="10" y="20">PTEN</text></g>
153
+ <g id="akt"><rect x="200" y="0" width="60" height="30"/><text x="210" y="20">AKT</text></g>
154
+ <g data-source="pten" data-target="akt">
155
+ <path d="M60 15 L200 15" stroke="black" marker-end="url(#arrow)"/>
156
+ <text x="110" y="10">inhibits</text>
157
+ </g>
158
+ ```
159
+
160
+ The attributes are not drawn, so they are only believed when the drawing
161
+ agrees: the connector's stroked path must join the two nodes' shapes, one end
162
+ on each (transforms applied, with a little slack for an arrowhead), and the
163
+ connector, its label and both nodes and their labels must all be visible.
164
+
165
+ Arrowheads drawn as markers give the direction:
166
+
167
+ - **One arrowhead** (a `marker-start` or `marker-end`, set on the path or
168
+ inherited) points at the node at that end, and the line reads from the other
169
+ node to it. This holds even when it's the reverse of `data-source` →
170
+ `data-target`: the drawing wins, as in drawio.
171
+ - **Arrowheads at both ends** make the line quotable both ways.
172
+ - **No arrowhead** (a plain line, or a marker that draws nothing) reads in the
173
+ attributes' order, `data-source` first. This is a known limitation: nothing
174
+ drawn gives that order, so swapping the two attributes reverses the line
175
+ for the same picture.
176
+ - **An arrowhead drawn as a separate shape** gives no edge line: which way
177
+ such a shape points can't be read, and the attributes' order could
178
+ contradict it. This is any other `<polygon>`, `<polyline>`, `<path>` or
179
+ `<line>` in the connector, wherever it sits; any `<use>` or `<image>`; and a
180
+ `<rect>`, `<circle>` or `<ellipse>` at an end of the line or on a node. A
181
+ `<rect>`, `<circle>` or `<ellipse>` in mid-line, such as a label's
182
+ background, is not taken for an arrowhead.
183
+ - **Arrowheads at a bent line's inner vertices** (`marker-mid`, or the
184
+ `marker` shorthand) give no edge line, since each may point either way.
185
+
186
+ A marker counts as an arrowhead when it names a `<marker>` in the figure that
187
+ draws something: a shape that paints, at a size above zero. When that can't be
188
+ told from the markup (a `var()` value, or a `<use>`, text or image inside the
189
+ marker), the connector gives no edge line, since neither direction is safe. The
190
+ marker's `orient` is not read: a marker drawn at a fixed angle is taken to
191
+ point along the path. A node is the shapes it draws inside the
192
+ element its id names; its text, and shapes that are hidden or paint nothing,
193
+ do not count towards its box. Geometry the extractor cannot place from
194
+ attributes gives no edge line either: CSS `transform` or other CSS geometry,
195
+ `clip-path`, `mask` or `filter`, or an animation that moves shapes. A node drawn only through `<use>`, inside a
196
+ nested `<svg>`, or sized in `%` or `em` gives no edge line.
197
+
144
198
  Known limits: a label drawn as several `<text>` elements, one per line, is
145
199
  read as several labels, so quote its lines with `...` between them. Text
146
200
  clipped by `clip-path` or `mask`, placed outside the
147
201
  `viewBox`, scaled to nothing, covered by another shape, drawn in the
148
202
  background colour, or cut short by CSS `text-overflow` or `-webkit-line-clamp`
149
203
  is still read. Text converted to outlines is not text and
150
- cannot be quoted. Native `.drawio` files and connector (edge) labels between
151
- nodes are not yet supported.
204
+ cannot be quoted.
205
+
206
+ ## Diagrams as References (drawio)
207
+
208
+ A `file:` reference can also point at a native drawio diagram, `.drawio` or
209
+ `.drawio.xml`, compressed or not. It is read the same way as an SVG: one label
210
+ per drawn cell, quotes held to one label, and an edge line
211
+ `"<source> <edge label> <target>"` for each drawn edge between two labelled
212
+ cells. In drawio an edge's source and target are what it is drawn between, so
213
+ no geometry check is needed; its line and both cells' labels must still be
214
+ visible. The arrows give the direction, as for SVG: an edge drawn pointing
215
+ back at its source reads target first, one with arrows at both ends reads both
216
+ ways, and one with no arrow reads source first. Only markers that point count
217
+ as arrows: arrowheads (`classic`, `block`, `open`, `async` and their thin or
218
+ double forms) and the inhibition T-bar `baseDash`. An oval, diamond, dash or
219
+ ER symbol at an end is not an arrow. A marker drawio doesn't list as either
220
+ gives no edge line. Every page of the file is read.
221
+
222
+ A `.drawio.svg` file (drawio's editable SVG export) is read as an SVG: its
223
+ labels are quotable, but it carries no `data-source`/`data-target`, so it
224
+ gives no edge lines. Point at the `.drawio` source to quote its edges.
225
+
226
+ Not read: cells marked invisible, in a hidden layer, or inside a collapsed
227
+ group; labels switched off (`noLabel`) or drawn with zero `opacity`,
228
+ `textOpacity` or `fontSize`, or with no `fontColor`; layer and page names,
229
+ tooltips, styles and other attributes. HTML labels follow the same visibility
230
+ rules as SVG `<foreignObject>` labels. A label computed from placeholders
231
+ (`placeholders="1"`), or HTML that cannot be read statically, makes the
232
+ reference `unavailable`.
152
233
 
153
234
  ## Using in Data Files
154
235
 
@@ -190,7 +271,8 @@ Both file and URL references work in LinkML data files:
190
271
 
191
272
  ## Limitations
192
273
 
193
- - **Local files are read as text**, apart from `.svg` figures (see above).
274
+ - **Local files are read as text**, apart from `.svg`, `.drawio` and `.drawio.xml` figures
275
+ (see above).
194
276
  An `.html` file is matched against its raw markup.
195
277
  - **Local PDF files**: Not yet supported as `file:` references. PDFs fetched by `url:` are supported.
196
278
  - **Authentication**: URLs requiring login are not supported
@@ -106,6 +106,23 @@ classes:
106
106
  title: "MUC1 oncoprotein blocks nuclear targeting of c-Abl"
107
107
  ```
108
108
 
109
+ The reference may take the same shapes as for excerpts:
110
+
111
+ - **A list of one** is checked like a single reference.
112
+ - **Several references with one title field** pass if the title is any one
113
+ reference's title, since it names one paper. If it is none of them, a single
114
+ error names each reference with its own reason. A reference whose prefix is
115
+ skipped vouches for nothing.
116
+ - **Reference objects with their own `title`**, listed or not, and with no title
117
+ field beside them, each have that title checked against their own reference.
118
+ - **A reference with no usable identifier** (an object without `id`, an empty
119
+ string) is reported as an error, not skipped.
120
+
121
+ With an excerpt as well, the title field is checked the same way: against the
122
+ one reference, or, with several, passing if it is any one reference's title.
123
+ It replaces any `title` a reference object carries. Without a title field,
124
+ each reference object's own `title` is checked with the excerpt.
125
+
109
126
  ## Combined Validation
110
127
 
111
128
  When both title and excerpt fields are present, both are validated together:
@@ -227,7 +227,22 @@ Ensure the cached content contains the text you're referencing.
227
227
  - Use `linkml-reference-validator cache reference url:https://... --force` to update cached content when pages change
228
228
  - Periodically review cached URLs to ensure they're still accessible
229
229
 
230
- ### 4. Mix Reference Types
230
+ ### 4. Cite Pages With Fragments
231
+
232
+ A fragment such as `#page=54` is never sent to the server, so it isn't part of
233
+ the cache key. `url:https://example.com/report.pdf#page=54` and
234
+ `...report.pdf#page=55` share one cache file, `url_https___example.com_report.pdf.md`,
235
+ and the document is fetched and extracted once. Each reference still carries
236
+ its own page-anchored URL, and quotes are matched against the whole document,
237
+ not the cited page. Query strings (`?v=2`) do stay part of the key.
238
+
239
+ Caches written before this change hold one file per fragment, such as
240
+ `url_https___example.com_report.pdf#page_54.md`. These are still read, until
241
+ the reference is next fetched (for example with `cache reference ... --force`);
242
+ the fresh entry goes to the shared file, after which the per-fragment files can
243
+ be deleted.
244
+
245
+ ### 5. Mix Reference Types
231
246
 
232
247
  URL references work alongside PMIDs and DOIs:
233
248
 
@@ -117,7 +117,22 @@ assert header.model_dump()["database"] == "Orphanet"
117
117
  ```
118
118
 
119
119
  The checker derives the filename from the stored `reference_id` by replacing
120
- `:`, `/`, `?`, and `=` with `_` and appending `.md`. DOI filenames are compared
120
+ `:`, `/`, `?`, and `=` with `_` and appending `.md`. A name that would exceed 200
121
+ bytes of UTF-8 is cut on a character boundary and ends in `_` plus the first 16
122
+ hex digits of the SHA-256 of the full `reference_id` (lowercased for a DOI), so
123
+ a long `url:` reference fits common filesystems' 255-byte limit and two long IDs
124
+ sharing a prefix still get different files. Shorter names are unchanged.
125
+
126
+ Because the replacement is lossy, distinct references can sanitize to the same
127
+ name, such as `url:https://ex.org/a?b=c` and `url:https://ex.org/a/b/c`. The
128
+ first one written keeps that name. When the name is already held by a file whose
129
+ `reference_id` is a different reference, the other one is written under the same
130
+ name ending in `_` and the 16-hex-digit hash of its own ID instead, and the
131
+ checker accepts that name for it. An entry is never served for a reference other
132
+ than the one it records: a hand-written entry whose `reference_id` is a
133
+ placeholder, or an alias spelling such as `pubmed:123` in `PMID_123.md`, is now
134
+ treated as a miss and fetched afresh under the disambiguated name. DOI filenames
135
+ are compared
121
136
  case-insensitively, matching the fetcher's existing behavior. Other filenames must
122
137
  match exactly. This uses the ID already stored in the file, without applying
123
138
  configured prefix aliases or requiring a currently registered reference source.