relaton 3.0.0.pre.alpha.17 → 3.0.0.pre.alpha.19

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/data/equivalence/adoption-chains.yaml +59 -0
  3. data/lib/relaton/3gpp/CLAUDE.md +187 -0
  4. data/lib/relaton/3gpp/README.adoc +149 -0
  5. data/lib/relaton/adobe/CLAUDE.md +104 -0
  6. data/lib/relaton/bib/CLAUDE.md +130 -0
  7. data/lib/relaton/bib/README.adoc +330 -0
  8. data/lib/relaton/bib/model/item_shared.rb +5 -1
  9. data/lib/relaton/bib/model/series.rb +1 -1
  10. data/lib/relaton/bipm/CLAUDE.md +173 -0
  11. data/lib/relaton/bipm/README.adoc +352 -0
  12. data/lib/relaton/bipm/data_fetcher.rb +3 -2
  13. data/lib/relaton/bsi/CLAUDE.md +35 -0
  14. data/lib/relaton/bsi/README.adoc +112 -0
  15. data/lib/relaton/calconnect/CLAUDE.md +177 -0
  16. data/lib/relaton/calconnect/README.adoc +140 -0
  17. data/lib/relaton/ccsds/CLAUDE.md +74 -0
  18. data/lib/relaton/ccsds/README.adoc +154 -0
  19. data/lib/relaton/cen/CLAUDE.md +219 -0
  20. data/lib/relaton/cen/README.adoc +155 -0
  21. data/lib/relaton/cie/CLAUDE.md +90 -0
  22. data/lib/relaton/cie/README.adoc +121 -0
  23. data/lib/relaton/core/CLAUDE.md +140 -0
  24. data/lib/relaton/core/README.adoc +64 -0
  25. data/lib/relaton/core/data_fetcher.rb +142 -3
  26. data/lib/relaton/db/CLAUDE.md +373 -0
  27. data/lib/relaton/db.rb +21 -1
  28. data/lib/relaton/doi/CLAUDE.md +105 -0
  29. data/lib/relaton/doi/README.adoc +89 -0
  30. data/lib/relaton/easc/CLAUDE.md +85 -0
  31. data/lib/relaton/ecma/CLAUDE.md +217 -0
  32. data/lib/relaton/ecma/README.adoc +178 -0
  33. data/lib/relaton/equivalence.rb +117 -0
  34. data/lib/relaton/etsi/CLAUDE.md +219 -0
  35. data/lib/relaton/etsi/README.adoc +137 -0
  36. data/lib/relaton/evs/CLAUDE.md +120 -0
  37. data/lib/relaton/evs/README.adoc +27 -0
  38. data/lib/relaton/gb/CLAUDE.md +47 -0
  39. data/lib/relaton/gb/README.adoc +169 -0
  40. data/lib/relaton/gost/CLAUDE.md +100 -0
  41. data/lib/relaton/iala/CLAUDE.md +97 -0
  42. data/lib/relaton/iana/CLAUDE.md +105 -0
  43. data/lib/relaton/iana/README.adoc +107 -0
  44. data/lib/relaton/iec/CLAUDE.md +13 -0
  45. data/lib/relaton/iec/README.adoc +280 -0
  46. data/lib/relaton/iec/data_fetcher.rb +24 -8
  47. data/lib/relaton/iec/project_fetcher.rb +48 -0
  48. data/lib/relaton/iec/project_parser.rb +111 -0
  49. data/lib/relaton/iec.rb +5 -0
  50. data/lib/relaton/ieee/CLAUDE.md +176 -0
  51. data/lib/relaton/ieee/README.adoc +131 -0
  52. data/lib/relaton/ietf/CLAUDE.md +357 -0
  53. data/lib/relaton/ietf/README.adoc +167 -0
  54. data/lib/relaton/iho/CLAUDE.md +70 -0
  55. data/lib/relaton/iho/README.adoc +119 -0
  56. data/lib/relaton/index/CLAUDE.md +207 -0
  57. data/lib/relaton/index/README.adoc +221 -0
  58. data/lib/relaton/index/dataset_lock.rb +122 -0
  59. data/lib/relaton/index/family.rb +63 -0
  60. data/lib/relaton/index/type.rb +32 -0
  61. data/lib/relaton/index.rb +3 -0
  62. data/lib/relaton/isbn/CLAUDE.md +47 -0
  63. data/lib/relaton/isbn/README.adoc +91 -0
  64. data/lib/relaton/iso/CLAUDE.md +38 -0
  65. data/lib/relaton/iso/README.adoc +370 -0
  66. data/lib/relaton/iso/data_fetcher.rb +16 -1
  67. data/lib/relaton/itu/CLAUDE.md +685 -0
  68. data/lib/relaton/itu/README.adoc +188 -0
  69. data/lib/relaton/jcgm/CLAUDE.md +107 -0
  70. data/lib/relaton/jis/CLAUDE.md +64 -0
  71. data/lib/relaton/jis/README.adoc +167 -0
  72. data/lib/relaton/logger/CLAUDE.md +37 -0
  73. data/lib/relaton/logger/README.adoc +120 -0
  74. data/lib/relaton/nist/CLAUDE.md +180 -0
  75. data/lib/relaton/nist/README.adoc +389 -0
  76. data/lib/relaton/nist/bibliography.rb +17 -1
  77. data/lib/relaton/nist/data_fetcher.rb +60 -4
  78. data/lib/relaton/nist/hit_collection.rb +121 -59
  79. data/lib/relaton/nist/scraper.rb +21 -7
  80. data/lib/relaton/oasis/CLAUDE.md +251 -0
  81. data/lib/relaton/oasis/README.adoc +132 -0
  82. data/lib/relaton/ogc/CLAUDE.md +242 -0
  83. data/lib/relaton/ogc/README.adoc +134 -0
  84. data/lib/relaton/ogc/data_fetcher.rb +9 -3
  85. data/lib/relaton/oiml/CLAUDE.md +47 -0
  86. data/lib/relaton/oiml/README.adoc +70 -0
  87. data/lib/relaton/omg/CLAUDE.md +137 -0
  88. data/lib/relaton/omg/README.adoc +101 -0
  89. data/lib/relaton/plateau/CLAUDE.md +126 -0
  90. data/lib/relaton/plateau/README.adoc +139 -0
  91. data/lib/relaton/sdo/CLAUDE.md +81 -0
  92. data/lib/relaton/un/CLAUDE.md +91 -0
  93. data/lib/relaton/un/README.adoc +125 -0
  94. data/lib/relaton/version.rb +1 -1
  95. data/lib/relaton/w3c/CLAUDE.md +369 -0
  96. data/lib/relaton/w3c/README.adoc +140 -0
  97. data/lib/relaton/w3c/data_fetcher.rb +1 -1
  98. data/lib/relaton/xsf/CLAUDE.md +189 -0
  99. data/lib/relaton/xsf/README.adoc +127 -0
  100. data/lib/relaton.rb +1 -0
  101. metadata +85 -4
@@ -0,0 +1,167 @@
1
+ = Relaton::Ietf
2
+
3
+ _Formerly known as_ `rfcbib`.
4
+
5
+ Relaton::Ietf is a Ruby gem that searches and fetches standards from the https://www.ietf.org[Internet Engineering Task Force (IETF)], including RFCs.
6
+
7
+ The standards scrape form https://www.rfc-editor.org/search/rfc_search.php
8
+
9
+ == Usage
10
+
11
+ === Fetching documents
12
+
13
+ [source,ruby]
14
+ ----
15
+ require 'relaton/ietf'
16
+ => true
17
+
18
+ # Fetch RFC document
19
+ item = Relaton::Ietf::Bibliography.get 'IETF RFC 8341'
20
+ relaton-ietf] INFO: (IETF RFC 8341) Fetching from Relaton repository ...
21
+ [relaton-ietf] INFO: (IETF RFC 8341) Found: `RFC 8341`
22
+ => #<Relaton::Ietf::ItemData:0x0000000125608cb0
23
+ ...
24
+
25
+ # Fetch Internet-Draft document
26
+ Relaton::Ietf::Bibliography.get 'IETF I-D.draft-abarth-cake-01'
27
+ [relaton-ietf] INFO: (IETF I-D.draft-abarth-cake-01) Fetching from Relaton repository ...
28
+ [relaton-ietf] INFO: (IETF I-D.draft-abarth-cake-01) Found: `draft-abarth-cake-01`
29
+ => #<Relaton::Ietf::ItemData:0x000000012141fdd0
30
+ ...
31
+
32
+ # Return nil if a document doesn't exist.
33
+ Relaton::Ietf::Bibliography.get 'IETF 1111'
34
+ [relaton-ietf] INFO: (IETF 1111) Fetching from Relaton repository ...
35
+ [relaton-ietf] INFO: (IETF 1111) Not found.
36
+ => nil
37
+ ----
38
+
39
+ === Serialization
40
+
41
+ [source,ruby]
42
+ ----
43
+ item.to_xml
44
+ => "<bibitem id="RFC8341" type="standard" schema-version="v1.4.1">
45
+ <fetched>2026-02-18</fetched>
46
+ <title type="main">Network Configuration Access Control Model</title>
47
+ <uri type="src">https://www.rfc-editor.org/info/rfc8341</uri>
48
+ <docidentifier type="IETF" primary="true">RFC 8341</docidentifier>
49
+ ....
50
+ </bibitem>"
51
+ ----
52
+ With `bibdata: true` option XML output is wrapped with `bibdata` element and `ext` element added.
53
+ [source,ruby]
54
+ ----
55
+ item.to_xml bibdata: true
56
+ => "<bibdata type="standard" schema-version="v1.4.1">
57
+ <fetched>2026-02-18</fetched>
58
+ <title type="main">Network Configuration Access Control Model</title>
59
+ <uri type="src">https://www.rfc-editor.org/info/rfc8341</uri>
60
+ <docidentifier type="IETF" primary="true">RFC 8341</docidentifier>
61
+ ...
62
+ <ext schema-version="v1.0.1">
63
+ <doctype>rfc</doctype>
64
+ <flavor>ietf</flavor>
65
+ <stream>IETF</stream>
66
+ </ext>
67
+ </bibdata>"
68
+ ----
69
+
70
+ === Typed source links
71
+
72
+ IETF documents may have `src`, `xml`, and `doi` source link types.
73
+
74
+ * `src` - web publication
75
+ * `xml` - BibXML publication
76
+ * `doi` - DOI reference
77
+
78
+ [source,ruby]
79
+ ----
80
+ item.source.first.type
81
+ => "src"
82
+ item.source.first.content
83
+ => "https://www.rfc-editor.org/info/rfc8341"
84
+ ----
85
+
86
+ === Parse a file locally
87
+
88
+ [source,ruby]
89
+ ----
90
+ item = Relaton::Ietf::BibXMLParser.parse_rfc File.read("spec/ietf/fixtures/rfc.xml")
91
+ => #<Relaton::Ietf::ItemData:0x000000012913e4d8
92
+ ...
93
+ ----
94
+
95
+ === Create bibliographic item from YAML
96
+ [source,ruby]
97
+ ----
98
+ Relaton::Ietf::Item.from_yaml File.read("spec/ietf/fixtures/item.yaml")
99
+ => #<Relaton::Ietf::ItemData:0x0000000127edf508
100
+ ...
101
+ ----
102
+
103
+ === Fetch data
104
+
105
+ There are IETF datasets that can be converted into RelatonXML/BibXML/BibYAML formats:
106
+
107
+ - `ietf-rfcsubseries` - https://www.rfc-editor.org/rfc-index.xml (`<bcp-entry>`, `<fyi-entry>`, `<std-entry>`)
108
+ - `ietf-internet-drafts` - convert files from local directory `./bibxml-ids`. Use `rsync -avcizxL rsync.ietf.org::bibxml-ids ./bibxml-ids` command to fetch the files.
109
+ - `ietf-rfc-entries` - https://www.rfc-editor.org/rfc-index.xml (`<rfc-entry>`)
110
+
111
+ The method `Relaton::Ietf::DataFetcher.fetch(source, output: "data", format: "yaml")` converts all the documents from the dataset and saves them to the `./data` folder in YAML format.
112
+
113
+ Arguments:
114
+
115
+ - `source` - dataset name (`ietf-rfcsubseries` or `ietf-internet-drafts`)
116
+ - `output` - folder to save documents (default './data').
117
+ - `format` - the format in which the documents are saved. Possible formats are: `yaml`, `xml`, `bibxml` (default `yaml`).
118
+
119
+ [source,ruby]
120
+ ----
121
+ Relaton::Ietf::DataFetcher.fetch "ietf-internet-drafts"
122
+ Started at: 2021-12-17 10:23:20 +0100
123
+ Stopped at: 2021-12-17 10:29:19 +0100
124
+ Done in: 1360 sec.
125
+ => nil
126
+ ----
127
+
128
+ === The index
129
+
130
+ Lookups read one combined index — `index-v2.zip` from
131
+ https://github.com/relaton/relaton-data-ietf[relaton-data-ietf], covering RFCs,
132
+ the RFC sub-series and Internet-Drafts in ~177k rows. `DataFetcher` writes the same
133
+ shape as it crawls, one row per record keyed by that record's primary
134
+ docidentifier, serialised as a `Pubid::Ietf::Identifier`:
135
+
136
+ [source,yaml]
137
+ ----
138
+ - :id:
139
+ _type: pubid:ietf:rfc
140
+ number: '3986'
141
+ :file: data/rfc3986.yaml
142
+ ----
143
+
144
+ A record whose identifier `Pubid::Ietf` cannot parse is still written, but is left
145
+ out of the index and logged — a single unparseable row would otherwise make the
146
+ whole index unloadable for every consumer. The total is restated when the crawl
147
+ finishes.
148
+
149
+ Note sub-series ids key to their own record: `STD 66` resolves to the STD document,
150
+ whose `relation: includes` names the RFC. There is no single RFC to alias to in
151
+ general — 42 of the 365 sub-series include more than one.
152
+
153
+ === Logging
154
+
155
+ Relaton::Ietf uses the relaton-logger gem for logging. By default, it logs to STDOUT. To change the log levels and add other loggers, read the https://github.com/relaton/relaton-logger#usage[relaton-logger] documentation.
156
+
157
+ == Development
158
+
159
+ The suite uses one cached index fixture, `spec/ietf/fixtures/ietf-index-v2.zip`, copied wholesale from https://github.com/relaton/relaton-data-ietf[relaton-data-ietf]. Refresh it by re-downloading that repo's `index-v2.zip` when a document lookup starts 404ing — a stale index names files that no longer exist, which surfaces as a 404 rather than as an index error.
160
+
161
+ == Contributing
162
+
163
+ Bug reports and pull requests are welcome on GitHub at https://github.com/metanorma/relaton-ietf.
164
+
165
+ == License
166
+
167
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,70 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What This Is
6
+
7
+ relaton-iho is a Ruby gem that fetches and serializes IHO (International Hydrographic Organization) standards metadata. It is a "flavor" gem in the Relaton ecosystem, extending relaton-bib with IHO-specific bibliographic data models.
8
+
9
+ ## Common Commands
10
+
11
+ ```bash
12
+ bundle exec rspec # run all tests
13
+ bundle exec rspec spec/relaton/iho/item_spec.rb # run a single spec file
14
+ bundle exec rspec spec/relaton/iho/item_spec.rb:15 # run a single example by line
15
+ bundle exec rubocop # lint
16
+ bundle exec rubocop -a # lint with auto-fix
17
+ bundle exec rake install # install gem locally
18
+ ```
19
+
20
+ ## Architecture
21
+
22
+ ### Flavor Pattern
23
+
24
+ This gem follows the Relaton flavor pattern: it subclasses base classes from `relaton-bib` under the `Relaton::Iho` namespace. The key mechanism is `NamespaceHelper` (from relaton-bib), which dynamically resolves the module namespace from the class name. This means `Relaton::Iho::ItemData#to_xml` automatically delegates to `Relaton::Iho::Bibdata.to_xml` (not `Relaton::Bib::Bibdata`).
25
+
26
+ Core model classes and their base classes:
27
+ - `Item < Bib::Item` — main bibliographic item, uses `model ItemData`
28
+ - `ItemData < Bib::ItemData` — data container enabling namespace-aware `to_xml`/`to_yaml`/`to_json`
29
+ - `ItemBase < Item` — stripped-down item for use inside relations (no id, schema_version, fetched, ext)
30
+ - `Bibitem < Item` — XML bibitem serialization (includes `Bib::BibitemShared`)
31
+ - `Bibdata < Item` — XML bibdata serialization (includes `Bib::BibdataShared`)
32
+ - `Relation < Bib::Relation` — overrides `bibitem` attribute to use `Iho::ItemBase`
33
+ - `Doctype < Bib::Doctype` — IHO document type vocabulary
34
+ - `Ext < Bib::Ext` — IHO-specific extension data (doctype, commentperiod)
35
+ - `Processor < Core::Processor` — Relaton processor for registry integration
36
+ - `HashParserV1` — converts v1-format YAML/Hash data to v2 model objects (required on demand, not auto-loaded)
37
+ - `Bibliography` — fetches IHO data from relaton-data-iho GitHub repo
38
+
39
+ All model classes use Lutaml::Model for declarative attribute/serialization definitions.
40
+
41
+ ### HashParserV1
42
+
43
+ `HashParserV1` (in `hash_parser_v1.rb`) handles conversion of legacy v1 data to the current model. It overrides the base `Bib::HashParserV1` to handle IHO-specific structures:
44
+ - **editorialgroup**: IHO v1 data has nested committee arrays (`editorialgroup: [[{committee: ...}, ...]]`). Each committee is converted to a `contributor` with `role type="author"` / `description="committee"`, organization "International Hydrographic Organization"/"IHO", and nested `subdivision` elements for sub-committees.
45
+ - **commentperiod**: Moved from top-level into `ext`.
46
+ - Factory methods (`bib_item`, `create_doctype`, `create_relation`) return IHO-specific classes.
47
+
48
+ This module is required on demand (`require "relaton/iho/hash_parser_v1"`) and not auto-loaded by the main `relaton/iho` entry point.
49
+
50
+ ### Load Order Constraint
51
+
52
+ In `item.rb`, `require_relative "relation"` is placed **after** the `Item` class definition because `Relation` → `ItemBase` → `Item` creates a circular dependency if loaded before `Item` exists.
53
+
54
+ ### Data Retrieval
55
+
56
+ `Bibliography.search`/`.get` fetches YAML from the relaton-data-iho GitHub repo via `Relaton::Index`, then deserializes into model objects.
57
+
58
+ ### Test Infrastructure
59
+
60
+ - **VCR cassettes** (`spec/vcr_cassettes/`): recorded HTTP responses for offline testing
61
+ - **RelaxNG validation**: specs validate XML output against `grammars/relaton-iho-compile.rng` using ruby-jing
62
+ - **Fixtures** (`spec/fixtures/`): reference XML/YAML documents for round-trip serialization tests
63
+
64
+ ### IHO-Specific Document Types
65
+
66
+ policy-and-procedures, best-practices, supporting-document, report, legal, directives, proposal, standard
67
+
68
+ ## Testing
69
+
70
+ - **Index fixture:** `spec/fixtures/index-v1.zip` is pre-loaded into `Relaton::Index` pool in `before(:suite)` (configured in `spec/support/webmock.rb`). Run `rake spec:update_index` to refresh from relaton-data-iho.
@@ -0,0 +1,119 @@
1
+ = Relaton for IHO documents
2
+
3
+ Relaton::Iho is a Ruby gem that implements the https://github.com/metanorma/metanorma-model-iso#iso-bibliographic-item[IsoBibliographicItem model].
4
+
5
+ It currently retrieves metadata of IHO Standards from https://github.com/relaton/relaton-data-iho.
6
+
7
+ == Usage
8
+
9
+ === Search for a standard using keywords
10
+
11
+ Reference can be specified with or without an edition. References without an edition will return the latest edition of the standard. References with an edition will return the standard with the specified edition.
12
+
13
+ [source,ruby]
14
+ ----
15
+ require 'relaton/iho'
16
+ => true
17
+
18
+ # Search for a standard using a reference without an edition
19
+ item = Relaton::Iho::Bibliography.search("IHO B-11")
20
+ [relaton-iho] INFO: (IHO B-11) Fetching from Relaton repository ...
21
+ [relaton-iho] INFO: (IHO B-11) Found: `B-11`
22
+ => #<Relaton::Iho::ItemData:0x0000000121f85aa8...
23
+
24
+ # Search for a standard using a reference with an edition
25
+ item = Relaton::Iho::Bibliography.search("IHO B-11 1.0.0")
26
+ [relaton-iho] INFO: (IHO B-11 1.0.0) Fetching from Relaton repository ...
27
+ [relaton-iho] INFO: (IHO B-11 1.0.0) Found: `B-11`
28
+ => #<Relaton::Iho::ItemData:0x0000000125b5bb90
29
+ ...
30
+ ----
31
+
32
+ === XML serialization
33
+ [source,ruby]
34
+ ----
35
+ item.to_xml
36
+ => "<bibitem id="B11" type="standard" schema-version="v1.4.1">
37
+ <fetched>2026-02-20</fetched>
38
+ <title language="en" type="main">IHO-IOC GEBCO Cook Book</title>
39
+ <title language="fr" type="main">Livre de recettes GEBCO OHI-COI</title>
40
+ <uri type="pdf">https://www.star.nesdis.noaa.gov/socd/lsa/GEBCO_Cookbook/documents/CookBook_20191031.pdf</uri>
41
+ <docidentifier type="IHO" primary="true">B-11</docidentifier>
42
+ ...
43
+ </bibitem>"
44
+ ----
45
+ With argument `bibdata: true` it outputs XML wrapped by `bibdata` element and adds flavor `ext` element.
46
+ [source,ruby]
47
+ ----
48
+ item.to_xml bibdata: true
49
+ => "<bibdata type="standard" schema-version="v1.4.1">
50
+ <fetched>2026-02-20</fetched>
51
+ <title language="en" type="main">IHO-IOC GEBCO Cook Book</title>
52
+ <title language="fr" type="main">Livre de recettes GEBCO OHI-COI</title>
53
+ <uri type="pdf">https://www.star.nesdis.noaa.gov/socd/lsa/GEBCO_Cookbook/documents/CookBook_20191031.pdf</uri>
54
+ <docidentifier type="IHO" primary="true">B-11</docidentifier>
55
+ ...
56
+ <ext schema-version="v1.0.0">
57
+ <flavor>iho</flavor>
58
+ </ext>
59
+ </bibdata>"
60
+ ----
61
+
62
+ === Get code
63
+ [source,ruby]
64
+ ----
65
+ Relaton::Iho::Bibliography.get "IHO B-11"
66
+ [relaton-iho] INFO: (IHO B-11) Fetching from Relaton repository ...
67
+ [relaton-iho] INFO: (IHO B-11) Found: `B-11`
68
+ => #<Relaton::Iho::ItemData:0x0000000125f530e0
69
+ ...
70
+ ----
71
+
72
+ === Typed links
73
+
74
+ Each IHO document has `html`, `website`, or `pdf` type link.
75
+
76
+ [source,ruby]
77
+ ----
78
+ item.source.first.type
79
+ => "pdf"
80
+
81
+ item.source.first.content
82
+ => "https://www.star.nesdis.noaa.gov/socd/lsa/GEBCO_Cookbook/documents/CookBook_20191031.pdf"
83
+ ----
84
+
85
+ === Create bibliographic item from XML
86
+ [source,ruby]
87
+ ----
88
+ Relaton::Iho::Item.from_xml File.read('spec/fixtures/bibdata.xml')
89
+ => #<Relaton::Iho::ItemData:0x0000000125f56920
90
+ ...
91
+ ----
92
+
93
+ === Create bibliographic item from YAML
94
+ [source,ruby]
95
+ ----
96
+ Relaton::Iho::Item.from_yaml File.read('spec/fixtures/item.yaml')
97
+ => #<Relaton::Iho::ItemData:0x000000012274ac98
98
+ ...
99
+ ----
100
+
101
+ === Logging
102
+
103
+ RelatonIho uses the relaton-logger gem for logging. By default, it logs to STDOUT. To change the log levels and add other loggers, read the https://github.com/relaton/relaton-logger#usage[relaton-logger] documentation.
104
+
105
+ == Development
106
+
107
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
108
+
109
+ To update the index test fixture (used by tests), run `rake spec:update_index`. This downloads the latest `index-v1.zip` from the https://github.com/relaton/relaton-data-iho[relaton-data-iho] repository.
110
+
111
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to https://rubygems.org[rubygems.org].
112
+
113
+ == Contributing
114
+
115
+ Bug reports and pull requests are welcome on GitHub at https://github.com/relaton/relaton-iho.
116
+
117
+ == License
118
+
119
+ The gem is available as open source under the terms of the https://opensource.org/licenses/MIT[MIT License].
@@ -0,0 +1,207 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ relaton-index is a Ruby gem that provides indexing and searching of Relaton document references. It maps document identifiers to file paths, supporting both local index creation (for publishing) and remote index consumption (downloading from URLs with 24-hour caching).
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Run all tests (default rake task)
13
+ rake spec
14
+
15
+ # Run linting
16
+ rake rubocop
17
+
18
+ # Run specific test file
19
+ bundle exec rspec spec/relaton/type_spec.rb
20
+
21
+ # Run specific test by name
22
+ bundle exec rspec spec/relaton/file_io_spec.rb -e "fetch_and_save"
23
+
24
+ # Install dependencies
25
+ bin/setup
26
+
27
+ # Interactive console
28
+ bin/console
29
+ ```
30
+
31
+ ## Architecture
32
+
33
+ ### Core Classes (all under `Relaton::Index` module in `lib/relaton/index/`)
34
+
35
+ - **`Relaton::Index`** (module, `lib/relaton/index.rb`) — Static API entry point. Delegates to Pool and Config. Main methods: `find_or_create`, `close`, `configure`.
36
+
37
+ - **Pool** — Object pool that caches Type instances by document type (`:ISO`, `:IEC`, `:IHO`, etc.). Reuses existing indexes if parameters match, recreates if they change.
38
+
39
+ - **Type** — Represents one index for a document type. Holds an array of `{id:, file:}` hashes. Provides `add_or_update`, `search` (subset match, string substring match, or block), and `save`.
40
+
41
+ - **FileIO** — Handles reading/writing/downloading index files. Three modes based on `@url`: string URL (download and cache to `~/.relaton/{type}/`), `true` (read local file from `~/.relaton/{type}/`), `nil` (read from current directory). Uses class-level Mutex for thread-safe downloads. Validates index format on load.
42
+
43
+ - **ShardSource** (`shard_source.rb`) — Reads the machine index on a data repo's Pages site (relaton#189): `index/manifest.json`, then the one `index/shard-NNNNN.json` at `Zlib.crc32(id.root.number.to_s) % shards`. Memory only, 24 h TTL, one `Mutex`. Built by `Type` when `find_or_create` gets `pages_url:`. Reuses `FileIO#deserialize_id`/`#deserialize_pubid` through a file-less `FileIO`.
44
+
45
+ - **FileStorage** — Storage abstraction module with `ctime`, `read`, `write`, `remove`. Can be replaced via `Config.storage=` for custom backends (e.g., S3).
46
+
47
+ - **Config** — Global configuration: `storage`, `storage_dir`, `filename` (default: "index.yaml").
48
+
49
+ ### Data Flow
50
+
51
+ 1. `Relaton::Index.find_or_create(:TYPE, url:, file:, pubid_class:, pages_url:)` → Pool looks up or creates Type (`id_keys:` is accepted for one release with a deprecation warning, then ignored)
52
+ 2. Type lazily loads index via FileIO on first access
53
+ 3. FileIO either reads local YAML or downloads ZIP from URL, extracts, validates format
54
+ 4. Search matches against `:id` field (string comparison via `include?` or custom block)
55
+ 5. `save` writes index as YAML to local file
56
+
57
+ With `pages_url:` (W3C, the #189 pilot) steps 2–3 change: a non-String query
58
+ goes to `ShardSource#rows` and reads one shard, and `Type#index` (a String query,
59
+ a block-only search) is `ShardSource#whole_index`, the `<index>.zip` that the
60
+ manifest names, read from the Pages site. No `FileIO#read`, no disk cache.
61
+
62
+ ### What `#search` matches by default
63
+
64
+ Search is narrow, then match. The **match** step, for two identifiers, is
65
+ pubid's asymmetric subset match `id === item[:id]` (`Pubid::SubsetMatch`): the
66
+ query is the reference, and a component it leaves nil or empty matches any
67
+ value. That is what a search by reference means, so a flavor whose optional
68
+ components are true wildcards passes **no block** — OASIS, W3C, XSF, IALA,
69
+ OGC, ECMA, 3GPP, CalConnect, GOST and Plateau each call
70
+ `index.search(pubid)`. A component that a flavor reads as "none" when nil is
71
+ declared `subset_strict` in pubid (pubid#408), so `===` compares it exactly.
72
+
73
+ A nil is a wildcard here, not "the document has none". A caller that needs
74
+ **exact** equality says so with `search(id, exact: true)`, which selects the
75
+ rows with `item[:id] == id`. The binary-search narrowing still runs first.
76
+ These callers do:
77
+
78
+ | caller | why |
79
+ |---|---|
80
+ | `Relaton::Ccsds::HitCollection#rows` (edition branch) | pubid's CCSDS `suffix` is a wildcard, so `CCSDS 101.0-B-4` would also reach the historical `101.0-B-4-S` — 260 such pairs in the index fixture. (The omitted language reached the translations too, until pubid#408 made `language` `subset_strict`.) |
81
+ | `Relaton::Ietf::Scraper#fetch_doc` | a draft slug omits the version, so `draft-foo` would match every `draft-foo-NN` and `.first` would pick one — 20,513 such pairs in the first 40k fixture rows |
82
+ | `Relaton::Gost::Bibliography#rows` (dated citation) | a dated citation names one edition; it replaces the old `query.matches?(row)` with no ignore list, which is `==` |
83
+ | `Relaton::Plateau::HitCollection#find` (reference with an edition) | a reference with an edition names that one row, as the old `row[:id] == ref` block did |
84
+ | `Relaton::Oiml::Bibliography#best_row` (a Bulletin) | a Bulletin's year, issue and sequence are its locator, so `OIML Bulletin 1960` is the volume and not every article of 1960 |
85
+
86
+ A String query keeps the substring match it always had, in both directions;
87
+ with `exact: true` it is compared with `==`. A block is a custom predicate for
88
+ any other rule, and `search` raises `ArgumentError` if it gets both a block and
89
+ `exact: true`.
90
+
91
+ ### The narrowing key — one expression, its call sites
92
+
93
+ Search is two-stage: narrow, then match. Narrowing binary-searches the index for
94
+ the run of entries whose **base document number** equals the query's, using the key
95
+ `id.root.number.to_s` (`#root` walks a supplement/amendment `.base` chain, so a
96
+ document and its wrappers share one key and cluster together).
97
+
98
+ **Narrowing only happens for non-String queries on a `pubid_class:` index** —
99
+ `search_candidates` loads the raw side first (that load is what settles
100
+ `@file_io.sorted`), then narrows. A String query scans the whole index
101
+ *and* matches via `item[:id].to_s.include?(id)`, which renders every pubid in it.
102
+ So a flavor on a `pubid_class:` index must query with parsed identifiers; querying
103
+ it with strings is slower than the plain-string index it replaced.
104
+
105
+ That expression is written out in these places, and they must all agree or
106
+ bsearch silently returns the wrong slice:
107
+
108
+ | file | method | role |
109
+ |---|---|---|
110
+ | `type.rb` | `candidates_by_number` | the query's key |
111
+ | `type.rb` | `bsearch_left` / `bsearch_right` | bounds, over the **precomputed keys array** |
112
+ | `type.rb` | `narrowing_key` | add-time key (root-guarded for String ids) |
113
+ | `file_io.rb` | `deserialize_pubid` | build-time sort of the materialized rows |
114
+ | `file_io.rb` | `warn_unless_sorted` | sortedness check |
115
+ | `file_io.rb` | `sort_structured_index` / `save_raw` | save-time sort |
116
+
117
+ ### Raw rows and the sidecar (relaton#242 stopgap)
118
+
119
+ `FileIO#read_raw` returns `[keys, rows]`: the rows as plain `to_hash` hashes —
120
+ no pubid objects — with a parallel array of precomputed root-number keys, sorted.
121
+ A **Marshal sidecar** (`<index>.yaml.ms`, `SIDECAR_VERSION`-stamped) holds them,
122
+ so a repeat load skips the YAML parse and the one-time full materialization that
123
+ computes the keys (objects exist only during that build; the raw hash cannot
124
+ walk `.base` itself). The sidecar is authoritative while it is at least as new
125
+ as the yaml — the yaml is not parsed until the sidecar is stale or unreadable —
126
+ and `save`/`remove` delete it. A parsed search materializes pubid objects only
127
+ for its bucket (`FileIO#materialize`); the whole graph materializes only through
128
+ `Type#index`, a String query, or a block. `add_or_update` stores raw rows
129
+ (`to_hash` when the id is a `pubid_class` instance, verbatim otherwise) with an
130
+ O(1) raw-side dedup by id-hash equality. Numbers: the 79,993-row ISO index is
131
+ ~311 MB of raw rows instead of a ~675 MB object graph; only the searched bucket
132
+ is materialized at search time.
133
+
134
+ **Consequence for pubid flavors:** once a flavor does query with parsed ids, an
135
+ identifier family whose `number` is nil keys every row to `""`, so they collapse
136
+ into one bucket and the bsearch buys nothing. It fails *silently* — results stay
137
+ correct, only speed drops. IETF Internet-Drafts are the worked example (see
138
+ `docs/data-repository-format.adoc`, "Two obligations a pubid-backed index
139
+ carries"); the fix belongs upstream, giving the family a real `number`, rather than
140
+ special-casing the key here.
141
+
142
+ ### Index Format
143
+
144
+ YAML array of hashes with `:id` (string or structured hash) and `:file` (path string). Supports backward compatibility with old string-based format and newer pubid object format. The full data-repository/index specification (schema, `:id` v1/v2 shapes, publishing + GitHub Pages contract) lives in `docs/data-repository-format.adoc` at the repo root.
145
+
146
+ ### Flavor `INDEXFILE` convention
147
+
148
+ Each flavor names its published index via a single constant in its top-level
149
+ `lib/relaton/<flavor>.rb`:
150
+
151
+ ```ruby
152
+ INDEXFILE = "index-vN".freeze # base name only — no extension
153
+ ```
154
+
155
+ **Rules:**
156
+ - The constant is spelled **`INDEXFILE`** (one word, no underscore) and holds the
157
+ **base name without extension** — `"index-v1"`, `"index-v2"`, etc.
158
+ - Call sites append the extension: `"#{INDEXFILE}.yaml"` for the local file passed
159
+ as `file:`, `"#{INDEXFILE}.zip"` for the published artifact appended to the
160
+ consumer `url:`. Never embed `.yaml`/`.zip` in the constant, and never hardcode
161
+ `index-vN.zip` at a call site — derive it from `INDEXFILE` so a version bump is
162
+ a one-line change.
163
+ - The version number encodes the index **structure** (`v1` = plain-string `:id`,
164
+ `v2` = pubid-hash `:id`); bumping it lets the previous gem line keep reading the
165
+ old file while the new structure is published under a new name.
166
+
167
+ ### What each flavor call site passes
168
+
169
+ A pubid flavor calls `find_or_create` from three places, and they do not need
170
+ the same arguments:
171
+
172
+ | call site | `url:` | `pubid_class:` |
173
+ |---|---|---|
174
+ | producer — `DataFetcher#index` | none (`file:` is CWD-relative) | **yes** — `FileIO#save` calls `to_hash` only for instances of it |
175
+ | consumer — `Bibliography`/`HitCollection#index` | the published `.zip` | **yes** — without it the rows stay raw hashes and `Type#search` stops narrowing |
176
+ | consumer on the Pages shards (W3C) | none; **`pages_url:`** | **yes** — a shard row is deserialized with it |
177
+ | delete — `Processor#remove_index_file` | **`true`** | **no** |
178
+
179
+ The delete path is `Type#remove_file` → `FileIO#remove` →
180
+ `FileStorage.remove(file)`. `FileIO#file` is built from `url`, the type's
181
+ directory and `file:` only, and the delete never reads or deserializes the
182
+ index, so `pubid_class:` has no effect on it. `url:` does: without it `file` is
183
+ the bare name, so a `Db#clear` on an empty pool deletes `./index-vN.yaml` in the
184
+ working directory and leaves `~/.relaton/<type>/` in place. OASIS shipped that
185
+ bug until `url: true` was added. `spec/index/relaton/type_spec.rb` and
186
+ `spec/oasis/relaton/oasis/processor_spec.rb` guard both facts.
187
+
188
+ ### Key Design Decisions
189
+
190
+ - **The shard path does not fall back.** A shard 404 is "not found" (`[]`), a
191
+ transport failure raises `Relaton::RequestError` so `Db#net_retry` retries it,
192
+ and an unreadable manifest/shard raises `Relaton::Index::Error`. Falling back to
193
+ the monolith would turn a Pages outage into a silent full download.
194
+ `Relaton::RequestError` is declared in `lib/relaton/core/request_error.rb`
195
+ (`Relaton::Core::RequestError` is an alias). `relaton/index` requires that one file,
196
+ not all of `relaton/core` or `relaton/bib`.
197
+ - **`Type#actual?` compares `pages_url`.** Without it `Pool#type` would keep
198
+ serving a zip-backed `Type` to a caller that asks for the shards, or the
199
+ reverse. A spec fixture that seeds the pool must answer `actual?` for
200
+ `pages_url:` as well (see `spec/w3c/support/webmock.rb`).
201
+ - **`Type#search` does not look at a loaded `@index` in shard mode.** A parsed
202
+ query always goes to its shard, so the 24 h TTL applies; `@index`, once
203
+ loaded, is not refreshed.
204
+ - Remote indexes cached for 24 hours at `~/.relaton/{type}/index.yaml`
205
+ - Thread safety via `@@mutex` in FileIO prevents concurrent downloads of the same file
206
+ - Pubid deserialization is optional — when `pubid_class` is provided, string IDs are converted to structured objects
207
+ - Index format validation checks for required `:id` and `:file` keys, with automatic recovery (re-download or removal) on corruption