relaton 3.0.7 → 3.1.0

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 (240) hide show
  1. checksums.yaml +4 -4
  2. data/bin/relaton +20 -0
  3. data/lib/relaton/3gpp/processor.rb +1 -1
  4. data/lib/relaton/3gpp.rb +3 -3
  5. data/lib/relaton/adobe/processor.rb +1 -1
  6. data/lib/relaton/adobe.rb +3 -3
  7. data/lib/relaton/bib/item_data.rb +2 -2
  8. data/lib/relaton/bib/model/docidentifier.rb +17 -2
  9. data/lib/relaton/bib.rb +2 -2
  10. data/lib/relaton/bibcollection.rb +146 -0
  11. data/lib/relaton/bibdata.rb +92 -0
  12. data/lib/relaton/bipm/data_outcomes_parser.rb +6 -1
  13. data/lib/relaton/bipm/processor.rb +1 -1
  14. data/lib/relaton/bipm.rb +3 -3
  15. data/lib/relaton/bsi/processor.rb +1 -1
  16. data/lib/relaton/bsi.rb +1 -1
  17. data/lib/relaton/calconnect/data_fetcher.rb +2 -2
  18. data/lib/relaton/calconnect/model/item.rb +1 -1
  19. data/lib/relaton/calconnect/processor.rb +1 -1
  20. data/lib/relaton/calconnect.rb +2 -2
  21. data/lib/relaton/ccsds/data/fetcher.rb +1 -1
  22. data/lib/relaton/ccsds/model/item.rb +1 -1
  23. data/lib/relaton/ccsds/processor.rb +1 -1
  24. data/lib/relaton/ccsds/util.rb +1 -1
  25. data/lib/relaton/ccsds.rb +1 -1
  26. data/lib/relaton/cen/processor.rb +1 -1
  27. data/lib/relaton/cen.rb +2 -2
  28. data/lib/relaton/cie/data_fetcher.rb +3 -3
  29. data/lib/relaton/cie/processor.rb +1 -1
  30. data/lib/relaton/cie.rb +2 -2
  31. data/lib/relaton/cli/base_convertor.rb +134 -0
  32. data/lib/relaton/cli/command.rb +396 -0
  33. data/lib/relaton/cli/data_fetcher.rb +20 -0
  34. data/lib/relaton/cli/frontend/dist/404.html +18 -0
  35. data/lib/relaton/cli/frontend/dist/app.iife.js +1 -0
  36. data/lib/relaton/cli/frontend/dist/style.css +3 -0
  37. data/lib/relaton/cli/frontend/index.html +14 -0
  38. data/lib/relaton/cli/frontend/package-lock.json +2945 -0
  39. data/lib/relaton/cli/frontend/package.json +29 -0
  40. data/lib/relaton/cli/frontend/public/404.html +18 -0
  41. data/lib/relaton/cli/frontend/src/App.test.ts +501 -0
  42. data/lib/relaton/cli/frontend/src/App.vue +506 -0
  43. data/lib/relaton/cli/frontend/src/app.ts +90 -0
  44. data/lib/relaton/cli/frontend/src/components/DocumentDetail.test.ts +142 -0
  45. data/lib/relaton/cli/frontend/src/components/DocumentDetail.vue +228 -0
  46. data/lib/relaton/cli/frontend/src/components/DocumentRow.vue +88 -0
  47. data/lib/relaton/cli/frontend/src/components/Icon.test.ts +44 -0
  48. data/lib/relaton/cli/frontend/src/components/Icon.vue +55 -0
  49. data/lib/relaton/cli/frontend/src/components/Pagination.vue +26 -0
  50. data/lib/relaton/cli/frontend/src/dev/main.ts +100 -0
  51. data/lib/relaton/cli/frontend/src/dev/sample-data.json +71 -0
  52. data/lib/relaton/cli/frontend/src/lib/filter.test.ts +88 -0
  53. data/lib/relaton/cli/frontend/src/lib/filter.ts +95 -0
  54. data/lib/relaton/cli/frontend/src/lib/hydrate.test.ts +102 -0
  55. data/lib/relaton/cli/frontend/src/lib/hydrate.ts +55 -0
  56. data/lib/relaton/cli/frontend/src/lib/shards.test.ts +319 -0
  57. data/lib/relaton/cli/frontend/src/lib/shards.ts +183 -0
  58. data/lib/relaton/cli/frontend/src/lib/types.ts +148 -0
  59. data/lib/relaton/cli/frontend/src/lib/url.test.ts +151 -0
  60. data/lib/relaton/cli/frontend/src/lib/url.ts +111 -0
  61. data/lib/relaton/cli/frontend/src/shims-vue.d.ts +5 -0
  62. data/lib/relaton/cli/frontend/src/styles/index.css +27 -0
  63. data/lib/relaton/cli/frontend/src/test/fetch.ts +58 -0
  64. data/lib/relaton/cli/frontend/tsconfig.json +20 -0
  65. data/lib/relaton/cli/frontend/vite.config.ts +42 -0
  66. data/lib/relaton/cli/frontend/vitest.config.ts +11 -0
  67. data/lib/relaton/cli/frontend_assets.rb +58 -0
  68. data/lib/relaton/cli/full_text_search.rb +98 -0
  69. data/lib/relaton/cli/i18n.yaml +1 -0
  70. data/lib/relaton/cli/index_item_normalizer.rb +380 -0
  71. data/lib/relaton/cli/index_site_generator.rb +1043 -0
  72. data/lib/relaton/cli/relaton_file.rb +290 -0
  73. data/lib/relaton/cli/subcommand_collection.rb +263 -0
  74. data/lib/relaton/cli/subcommand_db.rb +81 -0
  75. data/lib/relaton/cli/templates/_document.liquid +111 -0
  76. data/lib/relaton/cli/templates/_index.liquid +61 -0
  77. data/lib/relaton/cli/templates/index/page.liquid +20 -0
  78. data/lib/relaton/cli/templates/index-style.css +1017 -0
  79. data/lib/relaton/cli/util.rb +8 -0
  80. data/lib/relaton/cli/xml_convertor.rb +47 -0
  81. data/lib/relaton/cli/xml_to_html_renderer.rb +117 -0
  82. data/lib/relaton/cli/yaml_convertor.rb +102 -0
  83. data/lib/relaton/cli.rb +103 -0
  84. data/lib/relaton/core/processor.rb +1 -1
  85. data/lib/relaton/core.rb +1 -1
  86. data/lib/relaton/db/registry.rb +1 -1
  87. data/lib/relaton/db.rb +1 -1
  88. data/lib/relaton/doi/CLAUDE.md +1 -1
  89. data/lib/relaton/doi/parser.rb +2 -1
  90. data/lib/relaton/doi/processor.rb +1 -1
  91. data/lib/relaton/doi.rb +4 -4
  92. data/lib/relaton/easc/processor.rb +1 -1
  93. data/lib/relaton/easc.rb +3 -3
  94. data/lib/relaton/ecma/data_fetcher.rb +1 -1
  95. data/lib/relaton/ecma/processor.rb +1 -1
  96. data/lib/relaton/ecma.rb +2 -2
  97. data/lib/relaton/element_finder.rb +29 -0
  98. data/lib/relaton/etsi/data_fetcher.rb +2 -2
  99. data/lib/relaton/etsi/item.rb +1 -1
  100. data/lib/relaton/etsi/processor.rb +2 -2
  101. data/lib/relaton/etsi.rb +1 -1
  102. data/lib/relaton/evs/processor.rb +1 -1
  103. data/lib/relaton/evs.rb +2 -2
  104. data/lib/relaton/gb/CLAUDE.md +1 -1
  105. data/lib/relaton/gb/bibliography.rb +1 -1
  106. data/lib/relaton/gb/processor.rb +1 -1
  107. data/lib/relaton/gb.rb +2 -2
  108. data/lib/relaton/gost/processor.rb +1 -1
  109. data/lib/relaton/gost.rb +3 -3
  110. data/lib/relaton/iala/processor.rb +1 -1
  111. data/lib/relaton/iala.rb +3 -3
  112. data/lib/relaton/iana/data_fetcher.rb +1 -1
  113. data/lib/relaton/iana/processor.rb +1 -1
  114. data/lib/relaton/iana.rb +2 -2
  115. data/lib/relaton/iec/data_fetcher.rb +1 -1
  116. data/lib/relaton/iec/processor.rb +1 -1
  117. data/lib/relaton/iec.rb +2 -2
  118. data/lib/relaton/ieee/processor.rb +1 -1
  119. data/lib/relaton/ieee.rb +2 -2
  120. data/lib/relaton/ietf/CLAUDE.md +1 -1
  121. data/lib/relaton/ietf/data_fetcher.rb +1 -1
  122. data/lib/relaton/ietf/processor.rb +1 -1
  123. data/lib/relaton/ietf.rb +2 -2
  124. data/lib/relaton/iho/CLAUDE.md +3 -3
  125. data/lib/relaton/iho/bibliography.rb +41 -27
  126. data/lib/relaton/iho/hash_parser_v1.rb +1 -1
  127. data/lib/relaton/iho/processor.rb +4 -3
  128. data/lib/relaton/iho.rb +2 -2
  129. data/lib/relaton/index.rb +1 -1
  130. data/lib/relaton/isbn/processor.rb +1 -1
  131. data/lib/relaton/isbn.rb +1 -1
  132. data/lib/relaton/iso/hash_parser_v1.rb +1 -1
  133. data/lib/relaton/iso/processor.rb +1 -1
  134. data/lib/relaton/iso.rb +3 -3
  135. data/lib/relaton/itu/processor.rb +1 -1
  136. data/lib/relaton/itu.rb +3 -3
  137. data/lib/relaton/jcgm/processor.rb +1 -1
  138. data/lib/relaton/jcgm.rb +2 -2
  139. data/lib/relaton/jis/processor.rb +1 -1
  140. data/lib/relaton/jis.rb +2 -2
  141. data/lib/relaton/nist/CLAUDE.md +11 -1
  142. data/lib/relaton/nist/README.adoc +7 -1
  143. data/lib/relaton/nist/hit_collection.rb +25 -1
  144. data/lib/relaton/nist/mods_parser.rb +32 -8
  145. data/lib/relaton/nist/processor.rb +1 -1
  146. data/lib/relaton/nist/pubs_export.rb +69 -11
  147. data/lib/relaton/nist.rb +2 -2
  148. data/lib/relaton/oasis/processor.rb +1 -1
  149. data/lib/relaton/oasis.rb +2 -2
  150. data/lib/relaton/ogc/CLAUDE.md +1 -1
  151. data/lib/relaton/ogc/processor.rb +1 -1
  152. data/lib/relaton/ogc.rb +2 -2
  153. data/lib/relaton/oiml/processor.rb +1 -1
  154. data/lib/relaton/oiml.rb +2 -2
  155. data/lib/relaton/omg/CLAUDE.md +1 -1
  156. data/lib/relaton/omg/processor.rb +1 -1
  157. data/lib/relaton/omg.rb +1 -1
  158. data/lib/relaton/plateau/CLAUDE.md +1 -1
  159. data/lib/relaton/plateau/data_fetcher.rb +1 -1
  160. data/lib/relaton/plateau/processor.rb +1 -1
  161. data/lib/relaton/plateau.rb +2 -2
  162. data/lib/relaton/render/general.rb +319 -0
  163. data/lib/relaton/render/iso690/csl_import.rb +129 -0
  164. data/lib/relaton/render/iso690/element.rb +153 -0
  165. data/lib/relaton/render/iso690/elements/access.rb +44 -0
  166. data/lib/relaton/render/iso690/elements/authorizer.rb +45 -0
  167. data/lib/relaton/render/iso690/elements/citeid.rb +39 -0
  168. data/lib/relaton/render/iso690/elements/component_part.rb +97 -0
  169. data/lib/relaton/render/iso690/elements/creator.rb +267 -0
  170. data/lib/relaton/render/iso690/elements/edition.rb +97 -0
  171. data/lib/relaton/render/iso690/elements/extent.rb +71 -0
  172. data/lib/relaton/render/iso690/elements/identifier.rb +46 -0
  173. data/lib/relaton/render/iso690/elements/location.rb +73 -0
  174. data/lib/relaton/render/iso690/elements/medium.rb +32 -0
  175. data/lib/relaton/render/iso690/elements/numeration.rb +26 -0
  176. data/lib/relaton/render/iso690/elements/production.rb +111 -0
  177. data/lib/relaton/render/iso690/elements/pub_date.rb +63 -0
  178. data/lib/relaton/render/iso690/elements/series.rb +66 -0
  179. data/lib/relaton/render/iso690/elements/size.rb +45 -0
  180. data/lib/relaton/render/iso690/elements/status.rb +30 -0
  181. data/lib/relaton/render/iso690/elements/stddoc.rb +40 -0
  182. data/lib/relaton/render/iso690/elements/title.rb +56 -0
  183. data/lib/relaton/render/iso690/elements/updated.rb +38 -0
  184. data/lib/relaton/render/iso690/elements.rb +67 -0
  185. data/lib/relaton/render/iso690/fields.rb +142 -0
  186. data/lib/relaton/render/iso690/i18n/en.yml +48 -0
  187. data/lib/relaton/render/iso690/i18n/fr.yml +14 -0
  188. data/lib/relaton/render/iso690/i18n/ja.yml +38 -0
  189. data/lib/relaton/render/iso690/i18n.rb +83 -0
  190. data/lib/relaton/render/iso690/identifier_kinds.rb +29 -0
  191. data/lib/relaton/render/iso690/index_renderer.rb +57 -0
  192. data/lib/relaton/render/iso690/kinds.rb +53 -0
  193. data/lib/relaton/render/iso690/renderer.rb +150 -0
  194. data/lib/relaton/render/iso690/rules/bipm.rb +186 -0
  195. data/lib/relaton/render/iso690/rules/jis.rb +103 -0
  196. data/lib/relaton/render/iso690/rules/nist.rb +384 -0
  197. data/lib/relaton/render/iso690/rules.rb +227 -0
  198. data/lib/relaton/render/iso690/style/locale.rb +59 -0
  199. data/lib/relaton/render/iso690/style/name_form.rb +87 -0
  200. data/lib/relaton/render/iso690/style/scheme.rb +33 -0
  201. data/lib/relaton/render/iso690/style/sort_rule.rb +25 -0
  202. data/lib/relaton/render/iso690/style/template_map.rb +40 -0
  203. data/lib/relaton/render/iso690/style/type_template.rb +45 -0
  204. data/lib/relaton/render/iso690/style.rb +227 -0
  205. data/lib/relaton/render/iso690/styles/apa.yml +45 -0
  206. data/lib/relaton/render/iso690/styles/author-date.yml +31 -0
  207. data/lib/relaton/render/iso690/styles/chicago.yml +49 -0
  208. data/lib/relaton/render/iso690/styles/ieee-sa.yml +45 -0
  209. data/lib/relaton/render/iso690/styles/jis-en.yml +68 -0
  210. data/lib/relaton/render/iso690/styles/jis-ja.yml +67 -0
  211. data/lib/relaton/render/iso690/styles/lncs.yml +54 -0
  212. data/lib/relaton/render/iso690/styles/mla.yml +56 -0
  213. data/lib/relaton/render/iso690/template.rb +248 -0
  214. data/lib/relaton/render/iso690.rb +28 -0
  215. data/lib/relaton/sae/CLAUDE.md +89 -0
  216. data/lib/relaton/sae/bibdata.rb +8 -0
  217. data/lib/relaton/sae/bibitem.rb +8 -0
  218. data/lib/relaton/sae/bibliography.rb +128 -0
  219. data/lib/relaton/sae/data_fetcher.rb +219 -0
  220. data/lib/relaton/sae/doi_code.rb +83 -0
  221. data/lib/relaton/sae/item.rb +10 -0
  222. data/lib/relaton/sae/item_data.rb +6 -0
  223. data/lib/relaton/sae/processor.rb +62 -0
  224. data/lib/relaton/sae/util.rb +9 -0
  225. data/lib/relaton/sae.rb +37 -0
  226. data/lib/relaton/un/CLAUDE.md +1 -1
  227. data/lib/relaton/un/processor.rb +1 -1
  228. data/lib/relaton/un.rb +1 -1
  229. data/lib/relaton/version.rb +1 -1
  230. data/lib/relaton/w3c/bibliography.rb +1 -1
  231. data/lib/relaton/w3c/data_fetcher.rb +1 -1
  232. data/lib/relaton/w3c/processor.rb +1 -1
  233. data/lib/relaton/w3c.rb +2 -2
  234. data/lib/relaton/xsf/data_fetcher.rb +1 -1
  235. data/lib/relaton/xsf/processor.rb +1 -1
  236. data/lib/relaton/xsf.rb +2 -2
  237. data/lib/relaton-cli.rb +17 -0
  238. data/lib/relaton-render.rb +12 -0
  239. data/lib/relaton.rb +3 -2
  240. metadata +203 -23
data/lib/relaton/iala.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require "pubid"
2
- require "relaton/bib"
3
- require "relaton/index"
4
- require "relaton/version"
2
+ require_relative "bib"
3
+ require_relative "index"
4
+ require_relative "version"
5
5
 
6
6
  # Relaton::Iala is the IALA (International Organization for Marine Aids
7
7
  # to Navigation) flavor of the unified relaton gem. Adds typed Ext fields
@@ -1,4 +1,4 @@
1
- require "relaton/core"
1
+ require_relative "../core"
2
2
  require_relative "../iana"
3
3
  require_relative "parser"
4
4
 
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Iana
data/lib/relaton/iana.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "faraday"
4
- require "relaton/index"
4
+ require_relative "index"
5
5
  require "pubid"
6
- require "relaton/bib"
6
+ require_relative "bib"
7
7
  require_relative "version"
8
8
  require_relative "iana/util"
9
9
  require_relative "iana/item_data"
@@ -1,5 +1,5 @@
1
1
  require "yeptris"
2
- require "relaton/core"
2
+ require_relative "../core"
3
3
  require_relative "../iec"
4
4
  require_relative "data_parser"
5
5
 
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Iec
data/lib/relaton/iec.rb CHANGED
@@ -3,8 +3,8 @@ require "net/http"
3
3
  require "moxml"
4
4
  require "pubid"
5
5
  require "zip"
6
- require "relaton/index"
7
- require "relaton/iso"
6
+ require_relative "index"
7
+ require_relative "iso"
8
8
  require_relative "version"
9
9
  require_relative "iec/util"
10
10
  require_relative "iec/bibliography"
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Ieee
data/lib/relaton/ieee.rb CHANGED
@@ -2,8 +2,8 @@ require "digest/md5"
2
2
  require "faraday"
3
3
  require "yaml"
4
4
  require "pubid"
5
- require "relaton/index"
6
- require "relaton/bib"
5
+ require_relative "index"
6
+ require_relative "bib"
7
7
  require_relative "version"
8
8
  require_relative "ieee/util"
9
9
  require_relative "ieee/bibliography"
@@ -375,7 +375,7 @@ and the affected files come through with their real characters —
375
375
  - **An oracle for `read_bibxml` must read binary.** The method is byte-verbatim (`File.binread`), so comparing it against `File.read(path, encoding: "UTF-8")` compares two different strings **on Windows only**: Ruby opens a file in text mode there and collapses CRLF to LF. The repo declares no `.gitattributes` and the GitHub Windows runner image defaults to `core.autocrlf=true`, so a fixture stored with LF is checked out CRLF — which failed all three `windows-latest` jobs while every Linux and macOS job passed. Use `File.read(path, mode: "rb", encoding: "UTF-8")`. macOS cannot reproduce this by checking the fixture out as CRLF, because its text-mode read does not translate; emulate Windows explicitly with `File.read(path, encoding: "UTF-8", universal_newline: true)`. The two `keeps CRLF line endings verbatim` examples pin the contract with `binwrite` temp files, independent of the checkout — one per branch, since the CP1252 repair rebuilds the string run by run and is where a line ending is likeliest to be lost. Note that a byte-equality oracle alone cannot catch a dropped `force_encoding`: `rfc.xml` is pure ASCII, and Ruby compares two ASCII-only strings as equal across encodings, so the example asserts `encoding` separately.
376
376
  - **Schema validation** via `ruby-jing` against the shared RNG grammars in the repo-root `grammar/` (`Jing.new "../../grammar/relaton-ietf-compile.rng"`, relative to the suite's CWD `spec/ietf/`)
377
377
  - **Shared examples** for org/person parsing in `bibxml_parser_spec.rb` (`parse_org`, `parse_person`)
378
- - **`equivalent-xml`** gem used for XML comparison (`be_equivalent_to` matcher)
378
+ - **`canon`** gem used for XML comparison (`be_equivalent_to` matcher)
379
379
 
380
380
  ## RuboCop
381
381
 
@@ -2,7 +2,7 @@ require "etc"
2
2
  require "parallel"
3
3
  require "pubid"
4
4
  require "pubid/ietf"
5
- require "relaton/core"
5
+ require_relative "../core"
6
6
  require_relative "../ietf"
7
7
  require_relative "bibxml_parser"
8
8
  require_relative "rfc/index"
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Ietf
data/lib/relaton/ietf.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
- require "relaton/index"
5
- require "relaton/bib"
4
+ require_relative "index"
5
+ require_relative "bib"
6
6
  require_relative "version"
7
7
  require_relative "ietf/util"
8
8
  require_relative "ietf/item_data"
@@ -55,11 +55,11 @@ In `item.rb`, `require_relative "relation"` is placed **after** the `Item` class
55
55
 
56
56
  `Bibliography.search`/`.get` fetches YAML from the relaton-data-iho GitHub repo via `Relaton::Index`, then deserializes into model objects.
57
57
 
58
- **Editions and series.** The pubid `version` is the edition (`IHO B-12 3.0.0`; the grammar takes `a.b.c` only). A reference with no version answers with the **latest** edition: `recency_key` compares the segments as integers (`2.10.0` after `2.9.0`). It used to be `min_by` on the string, which returned the oldest.
58
+ **Editions and series (the pubid 2.0.5 component model, relaton#440).** The pubid attribute is `edition` (dotted `a.b.c`, `IHO B-12 3.0.0`; the pre-440 name `version` is gone). A reference with no edition answers with the **latest**: `recency_key` compares the segments as integers (`2.10.0` after `2.9.0`). It used to be `min_by` on the string, which returned the oldest.
59
59
 
60
- **Series are identified by year or volume, not an edition (relaton#407).** B-7, P-5 and P-6 carry a `year` (`IHO P-5 2024`), the IHR (P-1) a `volume` with an optional `issue` (`IHO P-1 Vol. 30 (2)`) — plain scalars added by pubid#635, mutually exclusive with `version` (the grammar's tail picks exactly one). The same `recency_key` orders each kind numerically, so a bare series query answers with the latest year/volume/issue. Matching (`pubid_match?`) treats a component the query omits as a wildcard: `IHO P-1 Vol. 30` matches any issue of volume 30. The comparison runs through pubid's `#exclude` — excluding `:volume` nils the `issue` with it (the pair is one scalar cluster) — the way BIPM's `stem_keys` does. The subdivisions (part/appendix/annex/supplement) stay strict: this flavor cannot use the `===` subset match, because pubid does not declare `subset_strict` for IHO's subdivisions and a nil `part` would read as "any part" (`IHO S-100` would answer with a part row).
60
+ **Serial locators are volumes (relaton#440 unified the year locators of relaton#407).** The yearbook year IS a volume: `IHO P-5 Vol. 2020` is canonical, the bare-year faces (`P-5 2020`, `P-5/2020`) parse as legacy aliases into the same `volume`, and the IHR is `P-1 Vol. 30` with `Issue 1` (no parens) or the short slash form `P-1/30`. A bare series query answers with the latest volume/issue. **Components can publish separately and carry their own edition:** `IHO S-66 Suppl 1 2.0.0` gives `supplement_edition`, `S-100 Part S-102 1.0.0` gives `part_edition`, annexes likewise — and the publication years in parens (`S-52 Annex A 4.0.3 (2021)`) are `*_year` metadata, not identity: a stated year pins the record, an omitted one wildcards. Matching (`pubid_match?`) treats a discriminator the query omits as a wildcard: `IHO P-1 Vol. 30` matches any issue of volume 30. The comparison runs through pubid's `#exclude` — excluding `:volume` nils the `issue`, excluding an `:appendix` nils its `appendix_subnumber` (scalar clusters) — the way BIPM's `stem_keys` does. The subdivisions (part/appendix/annex/supplement) stay strict: this flavor cannot use the `===` subset match, because pubid does not declare `subset_strict` for IHO's subdivisions and a nil `part` would read as "any part" (`IHO S-100` would answer with a part row).
61
61
 
62
- **The edition reaches the index only through the committed row.** A record prints `B-12` and keeps its edition in `edition:`, so `relaton index` cannot read a version from the docid. `MachineIndex#refine` (relaton-cli) takes the committed `index-v3.yaml` row when it adds only a key in `REFINABLE_KEYS` to the parse — `version`/`edition`/`volume`/`year`/`issue` (relaton#407 added the year/issue pair for the series rows). Before that, the Pages index (generated 2026-10-29) had no `version` in any of its 325 rows, so every versioned reference answered not-found (relaton-data-iho#20). The suite did not see it: `spec/fixtures/index-v3.zip` is the committed index, which has versions. The suite's fixture currently carries the **re-keyed** series rows (year/volume) ahead of the data repo: the placeholder `1.0.0` rows for p-1/p-1-2x/p-5/p-6/b-7 are replaced by `year`/`volume` rows — refresh the fixture from the live published zip once relaton-data-iho re-keys, and reconcile any file-name drift then.
62
+ **The edition reaches the index only through the committed row.** A record prints `B-12` and keeps its edition in `edition:`, so `relaton index` cannot read a version from the docid. `MachineIndex#refine` (relaton-cli) takes the committed `index-v3.yaml` row when it adds only a key in `REFINABLE_KEYS` to the parse — `version`/`edition`/`volume`/`year`/`issue` (relaton#407 added the year/issue pair for the series rows). Before that, the Pages index (generated 2026-10-29) had no `version` in any of its 325 rows, so every versioned reference answered not-found (relaton-data-iho#20). The suite did not see it: `spec/fixtures/index-v3.zip` is the committed index, which has versions. The suite's fixture carries the #48-era re-keyed series rows migrated to the #440 keys (`version`→`edition`/`<component>_edition`, year locators→`volume`; `support/migrate_index_fixture.rb` did the one-shot migration) — refresh the fixture from the live published zip once relaton-data-iho round 2 republishes, and reconcile any file-name drift then.
63
63
 
64
64
  ### Test Infrastructure
65
65
 
@@ -96,31 +96,37 @@ module Relaton
96
96
  )
97
97
  end
98
98
 
99
- # The sort key for the latest record in a bucket (relaton#407). Each
100
- # IHO identity carries exactly one trailing discriminator — a dotted
101
- # `version` (an edition, `B-12 3.0.0`), a series `year` (`P-5 2024`)
102
- # or a series `volume` with its `issue` (`P-1 Vol. 30 (2)`) — and the
103
- # key orders by that component, numerically: version segments compare
104
- # as integers (2.10.0 is later than 2.9.0), as do year, volume and
105
- # issue. A row with no discriminator at all is the oldest. Rows in
106
- # one bucket share the discriminator kind once the data is keyed;
107
- # while kinds coexist the array compare is deterministic, and a
108
- # year's bare [2024] beats a placeholder edition's [1, 0, 0].
99
+ # The sort key for the latest record in a bucket (relaton#407,
100
+ # reshaped by relaton#440's component model). Volume-locator series
101
+ # (P-1, P-5) carry a `volume` with an optional `issue` — the
102
+ # yearbook year IS a volume (`P-5 Vol. 2020`; the bare-year faces
103
+ # of the #48 re-key parse as legacy aliases), so the year locators
104
+ # fold into the volume ordering. Edition-keyed products carry a
105
+ # dotted `edition` (`B-12 3.0.0`), and a component published
106
+ # separately carries its own edition (`S-66 Suppl 1 2.0.0` →
107
+ # `supplement_edition`). Every component compares numerically
108
+ # segment by segment (2.10.0 after 2.9.0); a row with no
109
+ # discriminator at all is the oldest.
109
110
  def recency_key(id)
110
- if id.year
111
- [id.year.to_i]
112
- elsif id.volume
111
+ if id.volume
113
112
  [id.volume.to_i, id.issue.to_i]
114
113
  else
115
- id.version.to_s.split(".").map(&:to_i)
114
+ ed = id.edition || id.supplement_edition || id.part_edition ||
115
+ id.annex_edition || id.appendix_edition || id.volume_edition
116
+ ed.to_s.split(".").map(&:to_i)
116
117
  end
117
118
  end
118
119
 
119
- # The components that make an IHO document the one it is beyond the
120
- # series and subdivisions: the edition (`version`), or — for the
121
- # series identified by year or volume instead (B-7, P-1, P-5, P-6;
122
- # relaton#407) — the `year` or the `volume`/`issue` pair.
123
- DISCRIMINATOR_KEYS = %i[version year volume issue].freeze
120
+ # The identity components beyond the series and subdivisions
121
+ # (relaton#440): the base `edition` — or the component-level
122
+ # edition when the component publishes separately — plus the
123
+ # volume/issue pair. The publication years (`year`, `*_year`) are
124
+ # metadata, not identity: a stated year pins the record, an
125
+ # omitted one wildcards it.
126
+ DISCRIMINATOR_KEYS = %i[
127
+ edition volume issue
128
+ supplement_edition part_edition annex_edition appendix_edition volume_edition
129
+ ].freeze
124
130
 
125
131
  # Both `row_id` and `query` are Pubid::Iho::Identifiers::* instances.
126
132
  # Class identity pins the series (S/P/M/B/C). Subdivision keys
@@ -132,21 +138,29 @@ module Relaton
132
138
  # so `IHO S-100` would answer with a part row.
133
139
  #
134
140
  # The discriminator keys stay nil-tolerant: an unqualified `IHO B-11`
135
- # finds the latest edition, an unqualified `IHO P-5` the latest year,
136
- # and `IHO P-1 Vol. 30` any issue of that volume — while a stated
137
- # component must match exactly. The comparison runs through pubid's
138
- # `#exclude` (the primitive `matches?` is built on), so the rules the
139
- # flavor declares there ride along: excluding `:volume` nils the
140
- # `issue` with it. `stem_keys` in the BIPM flavor is the same shape.
141
+ # finds the latest edition, an unqualified `IHO P-5` the latest
142
+ # volume (the yearbook year — relaton#440 folded the year
143
+ # locators in), and `IHO P-1 Vol. 30` any issue of that volume —
144
+ # while a stated component must match exactly. The comparison runs
145
+ # through pubid's `#exclude` (the primitive `matches?` is built
146
+ # on), so the rules the flavor declares there ride along: excluding
147
+ # `:volume` nils the `issue` with it, and excluding an appendix
148
+ # nils its subnumber — the scalar clusters are one unit. `stem_keys`
149
+ # in the BIPM flavor is the same shape.
141
150
  def pubid_match?(row_id, query)
142
151
  row_id.class == query.class &&
143
152
  row_id.exclude(*wildcards(query)) == query.exclude(*wildcards(query))
144
153
  end
145
154
 
146
155
  # The discriminators the query leaves unset: wildcards for the
147
- # search, significant whenever the query states one.
156
+ # search, significant whenever the query states one — plus the
157
+ # publication years, which are stated-metadata: pinned when the
158
+ # query carries them, wildcarded when it does not.
148
159
  def wildcards(query)
149
- DISCRIMINATOR_KEYS.select { |key| query.public_send(key).nil? }
160
+ keys = DISCRIMINATOR_KEYS + %i[
161
+ year supplement_year part_year annex_year appendix_year volume_year
162
+ ]
163
+ keys.select { |key| query.public_send(key).nil? }
150
164
  end
151
165
  end
152
166
  end
@@ -1,4 +1,4 @@
1
- require "relaton/bib/hash_parser_v1"
1
+ require_relative "../bib/hash_parser_v1"
2
2
  require_relative "../iho"
3
3
 
4
4
  module Relaton
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Iho
@@ -26,9 +26,10 @@ module Relaton
26
26
  end
27
27
 
28
28
  # An IHO record prints `B-12` and keeps its edition in `edition`, so
29
- # the edition goes on the key as the version (relaton#408).
29
+ # the edition goes on the key (relaton#408). The component model
30
+ # (relaton#440) renamed the pubid attribute `version` -> `edition`.
30
31
  def item_cache_key(pubid, item)
31
- with_components pubid, version: item.edition&.content
32
+ with_components pubid, edition: item.edition&.content
32
33
  end
33
34
 
34
35
  # @param xml [String]
data/lib/relaton/iho.rb CHANGED
@@ -1,5 +1,5 @@
1
- require "relaton/bib"
2
- require "relaton/index"
1
+ require_relative "bib"
2
+ require_relative "index"
3
3
  require "pubid"
4
4
  require_relative "version"
5
5
  require_relative "iho/util"
data/lib/relaton/index.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require "yaml"
4
4
  require "zip"
5
5
  require "open-uri"
6
- require "relaton/logger"
6
+ require_relative "logger"
7
7
 
8
8
  require_relative "version"
9
9
  require_relative "core/request_error"
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Isbn
data/lib/relaton/isbn.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "net/http"
2
- require "relaton/bib"
2
+ require_relative "bib"
3
3
  require_relative "version"
4
4
  require_relative "isbn/util"
5
5
  require_relative "isbn/isbn"
@@ -1,4 +1,4 @@
1
- require "relaton/bib/hash_parser_v1"
1
+ require_relative "../bib/hash_parser_v1"
2
2
  require_relative "../iso"
3
3
 
4
4
  module Relaton
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Iso
data/lib/relaton/iso.rb CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  require "net/http"
4
4
  require "pubid"
5
- require "relaton/index"
5
+ require_relative "index"
6
6
  require "isoics"
7
- require "relaton/bib"
8
- require "relaton/core"
7
+ require_relative "bib"
8
+ require_relative "core"
9
9
  require_relative "version"
10
10
  require_relative "iso/util"
11
11
  require_relative "iso/item_data"
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Itu
data/lib/relaton/itu.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  require "mechanize"
2
2
  require "pubid"
3
3
  require "digest/md5"
4
- require "relaton/index"
5
- require "relaton/bib"
6
- require "relaton/core"
4
+ require_relative "index"
5
+ require_relative "bib"
6
+ require_relative "core"
7
7
  require_relative "version"
8
8
  require_relative "itu/util"
9
9
  require_relative "itu/item_data"
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Jcgm
data/lib/relaton/jcgm.rb CHANGED
@@ -1,5 +1,5 @@
1
- require "relaton/bib"
2
- require "relaton/index"
1
+ require_relative "bib"
2
+ require_relative "index"
3
3
  require "pubid"
4
4
  require_relative "version"
5
5
  require_relative "jcgm/util"
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "relaton/core/processor"
3
+ require_relative "../core/processor"
4
4
 
5
5
  module Relaton
6
6
  module Jis
data/lib/relaton/jis.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  require "net/http"
4
4
  require "mechanize"
5
5
  require "pubid"
6
- require "relaton/iso"
7
- require "relaton/index"
6
+ require_relative "iso"
7
+ require_relative "index"
8
8
  require_relative "version"
9
9
  require_relative "jis/util"
10
10
  require_relative "jis/item"
@@ -69,7 +69,8 @@ All data models inherit from `Lutaml::Model::Serializable` and use declarative a
69
69
  - **`#write_file` serializes before it indexes**, so a record that fails to serialize leaves no index row naming a file that was never written. That is how a `Bib::Date` in `Scraper.fetch_dates` hid: `Nist::ItemData` takes `Nist::Date`, and YAML serialization rejects the parent class (the same strictness as the docidentifier note below).
70
70
  - `ModsParser` — maps MODS XML (via `loc_mods`) to `ItemData`
71
71
  - `Scraper` — fetches items from GitHub YAML or CSRC JSON
72
- - `PubsExport` — singleton; caches CSRC pubs-export zip with thread-safe daily updates
72
+ - `PubsExport` — singleton; caches CSRC pubs-export zip with thread-safe daily updates. The cache file is `#datafile`, under `Relaton::Index.config.storage_dir` (default `Dir.home`; the `DATAFILE` constant is only that default).
73
+ - **A read-only file system must not stop a search (relaton-nist#57).** When the cache write fails, `#fetch_data` keeps the downloaded zip in `@buffer`, and `#unzip` reads it with `Zip::File.open_buffer`. In rubyzip 3, `open_buffer` returns the buffer, not the block value, so collect the rows in a local. The cache write goes to a temp file and is renamed into place (`#write_datafile`), so a failed write never leaves a truncated zip that later processes read as fresh. `HitCollection#search` also rescues the local-cache errors in `LOCAL_DATA_ERRORS` from `from_json` (`#from_json_or_empty`), logs a warning, and continues with `from_ga`. Do not widen that list to `SystemCallError`: `Errno::ECONNREFUSED` is one, and a CSRC outage would then answer "not found" from the index, which `Relaton::Db` can cache. Specs assert the warning with `output(...).to_stderr_from_any_process`. A `receive(:warn)` stub on `Util` is never hit: `Kernel#warn` is private, the stub stays private, and the explicit-receiver call goes to `method_missing`.
73
74
 
74
75
  ### Pubid-backed docidentifier
75
76
 
@@ -115,6 +116,15 @@ NIST-specific gotchas (why this isn't a straight copy of IEC/CCSDS):
115
116
  numbered editions/revisions (`e2`, `r5`). Dates that ride the `update`
116
117
  component (some NBS supplements) are left intact — `update` also encodes
117
118
  non-date update codes (`/Upd2`), so stripping it would corrupt identity.
119
+ - **The item's `edition` comes from the DOI (relaton-nist#56).**
120
+ `ModsParser#parse_edition` sets `<edition number="N">N</edition>` only for a
121
+ numbered `e<N>` edition in the DOI (`NBS.CIRC.111e2` → 2, `11e2.1915` → 2):
122
+ 250 of the 20,392 live MODS records. An `e<year>` edition is a date, and an
123
+ `r<N>` revision is not an edition, so both stay without one. The MODS
124
+ `<edition>` element is prose (`New version`, `Erratum`), never a number. The
125
+ issue's "edition 0 → 1" rule came from the old Crossref feed; it is kept for
126
+ a DOI `e0` (none in the live data). A DOI pubid cannot parse gives no
127
+ edition, not an error.
118
128
  - **No `(all parts)` marker in `content`.** `content` is live-derived from
119
129
  `@pubid`, so `to_all_parts!` freezes the already-stripped rendering into
120
130
  `@raw_content` before wrapping `@pubid` in pubid's `AllParts` — matching
@@ -311,7 +311,13 @@ Relaton::Nist::Item.from_yaml File.read("spec/fixtures/item.yaml")
311
311
 
312
312
  === Fetching data
313
313
 
314
- On a search, the CSRC data sources are fetched and stored in the user's cache.
314
+ On a search, the CSRC data sources are fetched and stored in the user's cache,
315
+ `~/.relaton/nist/pubs-export.zip`. The cache follows
316
+ `Relaton::Index.config.storage_dir`, so one setting moves it together with the
317
+ index cache. When the cache cannot be written (for example on a read-only file
318
+ system such as AWS Lambda), the data stays in memory for the process. When the
319
+ CSRC data cannot be used at all, the search continues with the
320
+ `relaton-data-nist` index.
315
321
 
316
322
  This gem uses the https://github.com/usnistgov/NIST-Tech-Pubs/releases/download/May2024/allrecords-MODS.xml dataset as NIST-Tech-Pubs data source.
317
323
 
@@ -12,6 +12,12 @@ module Relaton
12
12
 
13
13
  GHNISTDATA = "https://raw.githubusercontent.com/relaton/relaton-data-nist/v2/"
14
14
 
15
+ # Errors of the local CSRC cache (read-only or full disk, a bad zip).
16
+ LOCAL_DATA_ERRORS = [
17
+ Errno::EROFS, Errno::EACCES, Errno::EPERM, Errno::ENOSPC,
18
+ Errno::ENOENT, Zip::Error
19
+ ].freeze
20
+
15
21
  attr_reader :reference
16
22
  attr_accessor :array
17
23
 
@@ -47,7 +53,7 @@ module Relaton
47
53
  # @return [Relaton::Nist::HitCollection] hits collection
48
54
  #
49
55
  def search
50
- @array = from_json
56
+ @array = from_json_or_empty
51
57
  @array = from_ga unless @array.any?
52
58
  sort_hits!
53
59
  end
@@ -400,6 +406,24 @@ module Relaton
400
406
  select_data.map { |h| Hit.new(hit_data(h), self) }
401
407
  end
402
408
 
409
+ #
410
+ # The CSRC hits, or none when the local CSRC data cannot be used: a
411
+ # file-system error must not stop the search, because the
412
+ # relaton-data-nist index can still answer (relaton-nist#57). Network
413
+ # errors are not rescued: they still reach Bibliography.search, which
414
+ # raises Relaton::RequestError, so a CSRC outage is not cached as
415
+ # "not found".
416
+ #
417
+ # @return [Array<Relaton::Nist::Hit>] hits
418
+ #
419
+ def from_json_or_empty
420
+ from_json
421
+ rescue *LOCAL_DATA_ERRORS => e
422
+ Util.warn "CSRC pubs-export data is not available (#{e.message}); " \
423
+ "searching the Relaton repository only", key: @reference
424
+ []
425
+ end
426
+
403
427
  def pubs_export_id(json)
404
428
  self.class.pubs_export_id json
405
429
  end
@@ -13,7 +13,7 @@ module Relaton
13
13
  }.freeze
14
14
 
15
15
  ATTRS = %i[type docidentifier title source abstract date contributor
16
- relation place series].freeze
16
+ relation place series edition].freeze
17
17
 
18
18
  def initialize(doc, series, errors = {})
19
19
  @doc = doc
@@ -51,17 +51,41 @@ module Relaton
51
51
  def get_id_from_str(str)
52
52
  return if str.nil? || str.empty?
53
53
 
54
- # DOIs arrive dotted ("NIST.HB.135e2022-upd1"); that MR-style form
55
- # parses through a path that skips UpdateCodes date lookup and drops
56
- # letter suffixes. Space the leading "PUB.SERIES." prefix so it parses
57
- # like the canonical short form (only the separator dots are touched,
58
- # not any inside the number). Then force :human rendering.
59
- spaced = str.sub(/\A(NIST|NBS)\.([A-Z][A-Za-z]*)\./, '\1 \2 ')
60
- ::Pubid::Nist::Identifier.parse(spaced).to_s(format: :human)
54
+ parse_pubid(str).to_s(format: :human)
61
55
  rescue ::Pubid::Errors::ParseError
62
56
  str.gsub(".", " ").sub(/^[\D]+/, &:upcase)
63
57
  end
64
58
 
59
+ # DOIs arrive dotted ("NIST.HB.135e2022-upd1"); that MR-style form
60
+ # parses through a path that skips UpdateCodes date lookup and drops
61
+ # letter suffixes. Space the leading "PUB.SERIES." prefix so it parses
62
+ # like the canonical short form (only the separator dots are touched,
63
+ # not any inside the number).
64
+ def parse_pubid(str)
65
+ spaced = str.sub(/\A(NIST|NBS)\.([A-Z][A-Za-z]*)\./, '\1 \2 ')
66
+ ::Pubid::Nist::Identifier.parse(spaced)
67
+ end
68
+
69
+ # The edition number a DOI states ("NBS.CIRC.111e2" -> 2), relaton-nist#56.
70
+ # The MODS carries no edition number of its own (its <edition> is prose:
71
+ # "New version", "Erratum"), so the DOI is the only source. Only a
72
+ # numbered "e<N>" edition counts: an "e<year>" edition is a date, and an
73
+ # "r<N>" revision is not an edition. An edition 0 counts as 1 (the issue's
74
+ # second rule; no live DOI has one).
75
+ # @return [Bib::Edition, nil]
76
+ def parse_edition
77
+ doi = parse_doi
78
+ return if doi.nil? || doi.empty?
79
+
80
+ edition = parse_pubid(doi).edition
81
+ return unless edition&.type == "e" && edition.id.to_s.match?(/\A\d{1,3}\z/)
82
+
83
+ number = edition.id.to_i.zero? ? "1" : edition.id
84
+ Bib::Edition.new(number: number, content: number)
85
+ rescue ::Pubid::Errors::Error, ArgumentError
86
+ nil
87
+ end
88
+
65
89
  # @return [String]
66
90
  def replace_wrong_doi(id)
67
91
  case id
@@ -1,4 +1,4 @@
1
- require "relaton/core/processor"
1
+ require_relative "../core/processor"
2
2
 
3
3
  module Relaton
4
4
  module Nist