relaton 3.0.3 → 3.0.5

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/relaton/3gpp/data_fetcher.rb +3 -2
  3. data/lib/relaton/bib/CLAUDE.md +24 -4
  4. data/lib/relaton/bib/README.adoc +64 -0
  5. data/lib/relaton/bib/converter/bibxml/from_rfcxml.rb +12 -1
  6. data/lib/relaton/bib/converter/bibxml/to_rfcxml.rb +32 -8
  7. data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup.rb +10 -3
  8. data/lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup_v3.rb +43 -0
  9. data/lib/relaton/bib/converter/bibxml/to_rfcxml_v3.rb +416 -0
  10. data/lib/relaton/bib/converter/bibxml.rb +21 -9
  11. data/lib/relaton/bib/item_data.rb +19 -7
  12. data/lib/relaton/bib/model/abstract.rb +5 -0
  13. data/lib/relaton/bib/model/localized_string.rb +6 -1
  14. data/lib/relaton/bib/sanitizer.rb +20 -5
  15. data/lib/relaton/bipm/data_outcomes_parser.rb +2 -1
  16. data/lib/relaton/bipm/si_brochure_parser.rb +3 -2
  17. data/lib/relaton/core/data_fetcher.rb +3 -2
  18. data/lib/relaton/core/processor.rb +29 -0
  19. data/lib/relaton/db/CLAUDE.md +14 -2
  20. data/lib/relaton/db/cache.rb +5 -2
  21. data/lib/relaton/db.rb +7 -3
  22. data/lib/relaton/ecma/processor.rb +8 -0
  23. data/lib/relaton/equivalence.rb +2 -2
  24. data/lib/relaton/gb/scraper.rb +2 -2
  25. data/lib/relaton/ieee/data_fetcher.rb +2 -1
  26. data/lib/relaton/ietf/bibxml_parser.rb +6 -0
  27. data/lib/relaton/iho/CLAUDE.md +4 -2
  28. data/lib/relaton/iho/bibliography.rb +44 -13
  29. data/lib/relaton/iho/processor.rb +6 -0
  30. data/lib/relaton/index/file_io.rb +9 -8
  31. data/lib/relaton/index/shard_source.rb +3 -2
  32. data/lib/relaton/iso/hit_collection.rb +20 -3
  33. data/lib/relaton/jcgm/meetings_parser.rb +2 -2
  34. data/lib/relaton/nist/cswp.rb +2 -2
  35. data/lib/relaton/nist/data_fetcher.rb +3 -2
  36. data/lib/relaton/sdo/store.rb +3 -1
  37. data/lib/relaton/version.rb +1 -1
  38. data/lib/relaton/xsf/hit.rb +2 -1
  39. metadata +10 -8
@@ -134,6 +134,21 @@ module Relaton
134
134
  copy
135
135
  end
136
136
 
137
+ # A copy of `pubid` with each given component the parse leaves nil, for
138
+ # #item_cache_key. `pubid` itself when there is nothing to add.
139
+ #
140
+ # @param pubid [Pubid::Identifier]
141
+ # @param values [Hash{Symbol => String, nil}]
142
+ # @return [Pubid::Identifier]
143
+ def with_components(pubid, **values)
144
+ values = values.select { |attr, value| value && pubid.public_send(attr).nil? }
145
+ return pubid if values.empty?
146
+
147
+ copy = pubid.class.from_hash pubid.to_hash
148
+ values.each { |attr, value| copy.public_send "#{attr}=", value }
149
+ copy
150
+ end
151
+
137
152
  # The cache key for a query: the parsed pubid with the `year` and
138
153
  # `all_parts` options folded in. The publication date range is not part
139
154
  # of the identity, so it never enters the key.
@@ -153,6 +168,20 @@ module Relaton
153
168
  pubid
154
169
  end
155
170
 
171
+ # The cache key of a fetched document: its primary docidentifier as
172
+ # parsed (`pubid`), plus the components that docid omits by design and
173
+ # the item keeps elsewhere. A flavor whose printed docid drops the
174
+ # edition (IHO `B-12`, ECMA `ECMA-262`) overrides this, or every
175
+ # edition gets one key and one cache file (relaton#408). The same set
176
+ # as relaton-cli's `MachineIndex::REFINABLE_KEYS`.
177
+ #
178
+ # @param pubid [Pubid::Identifier] the docid's parse; not mutated
179
+ # @param _item [Relaton::Bib::ItemData] the fetched document
180
+ # @return [Pubid::Identifier]
181
+ def item_cache_key(pubid, _item)
182
+ pubid
183
+ end
184
+
156
185
  def initialize
157
186
  raise "This is an abstract class!"
158
187
  end
@@ -223,6 +223,17 @@ relaton#189 item 2 / relaton#204. Two lutaml-store `FileSystem` stores under
223
223
  both naming the same file. There are no redirect entries. A document is
224
224
  deleted with the last row that points to it; a row whose document is gone is
225
225
  a miss.
226
+ - **The item row's key must tell editions apart (relaton#408).** `Db#item_key`
227
+ parses the primary docidentifier and passes it through
228
+ `Core::Processor#item_cache_key(pubid, item)`. A flavor whose printed docid
229
+ omits the edition by design overrides the hook to add it from the item:
230
+ IHO adds `version` from `edition` (`IHO B-12` → `IHO B-12 3.0.0`), ECMA adds
231
+ `edition` and the `volume` extent (`ECMA-262` → `ECMA-262 ed16`), through
232
+ `#with_components`, which fills only components the parse leaves nil, on a
233
+ copy. Without it every edition got one item key and one file, and a fetch of
234
+ `IHO B-12 2.0.3` overwrote the file the `IHO B-12 3.0.0` query row named. A
235
+ cache with such a file is dropped by the next `Relaton::VERSION` bump
236
+ (`grammar_hash`), so no separate cache-version bump was made.
226
237
 
227
238
  Matching: the **exact** row first (canonical `to_hash`, not pubid `==`, which
228
239
  is false after a `from_hash` round trip for some flavors, e.g. UN). Only a
@@ -233,8 +244,9 @@ still applies and it never answers with another query's year-stripped copy.
233
244
  `ISO 19115:2003 === ISO 19115-1:2003` (and the same for IEC, BSI, JIS, GB).
234
245
  `Cache#subset_of?` therefore also requires that every component the row adds
235
246
  (at any depth) is allowed: only a language for the dated fallback
236
- (`LANGUAGE_COMPONENTS`), a year/date/month or a language for the date-range
237
- `candidates` (`EDITION_COMPONENTS`). A row with no `root.number` (DOI, ISBN)
247
+ (`LANGUAGE_COMPONENTS`), a year/date/month, an edition/version (the
248
+ `item_cache_key` components; not a volume, which names another document) or a
249
+ language for the date-range `candidates` (`EDITION_COMPONENTS`). A row with no `root.number` (DOI, ISBN)
238
250
  goes to one of 256 digest buckets (`<flavor>/~<hex>`).
239
251
 
240
252
  Expiry (`Cache#valid_row?`): an undated row, and **every** `not_found` row,
@@ -36,8 +36,11 @@ module Relaton
36
36
  # reads an omitted `part` as "any part" (`ISO 19115:2003 ===
37
37
  # ISO 19115-1:2003`), which names another document.
38
38
  LANGUAGE_COMPONENTS = %w[language languages].freeze
39
- # What a row may add to a key and still be one of its editions.
40
- EDITION_COMPONENTS = (%w[year date month] + LANGUAGE_COMPONENTS).freeze
39
+ # What a row may add to a key and still be one of its editions. An
40
+ # `edition` or a `version` comes from Core::Processor#item_cache_key
41
+ # (IHO, ECMA; relaton#408). A `volume` names another document.
42
+ EDITION_COMPONENTS =
43
+ (%w[year date month edition version] + LANGUAGE_COMPONENTS).freeze
41
44
 
42
45
  # @return [String]
43
46
  attr_reader :dir
data/lib/relaton/db.rb CHANGED
@@ -382,13 +382,17 @@ module Relaton
382
382
  [query, query && processor.cache_key(code, year, opts, query)]
383
383
  end
384
384
 
385
- # The key of a fetched document, from its primary identifier. The
386
- # identifier is data, so an unparseable one gives no key.
385
+ # The key of a fetched document, from its primary identifier and what
386
+ # that identifier omits (Core::Processor#item_cache_key, relaton#408).
387
+ # The identifier is data, so an unparseable one gives no key.
387
388
  def item_key(bib, stdclass)
388
389
  docid = bib.docidentifier.detect(&:primary) || bib.docidentifier.first
389
390
  return unless docid&.content
390
391
 
391
- cache_key docid.content, nil, {}, stdclass
392
+ key = cache_key docid.content, nil, {}, stdclass
393
+ return key unless key.is_a? ::Pubid::Identifier
394
+
395
+ @registry[stdclass].item_cache_key key, bib
392
396
  rescue ::Pubid::Errors::Error
393
397
  nil
394
398
  end
@@ -21,6 +21,14 @@ module Relaton
21
21
  Bibliography.get(code, date, opts)
22
22
  end
23
23
 
24
+ # An ECMA record prints `ECMA-262` with no ` ed<N>`/` vol<N>`, and keeps
25
+ # them in `edition` and in a `volume` extent (relaton#408).
26
+ def item_cache_key(pubid, item)
27
+ volume = item.extent.flat_map(&:locality)
28
+ .detect { |l| l.type == "volume" }&.reference_from
29
+ with_components pubid, edition: item.edition&.content, volume: volume
30
+ end
31
+
24
32
  #
25
33
  # Fetch all the documents from a source
26
34
  #
@@ -1,4 +1,4 @@
1
- require "yaml"
1
+ require "yeptris/psych"
2
2
 
3
3
  module Relaton
4
4
  # Cross-SDO equivalence as data: chain patterns and explicit equivalences
@@ -23,7 +23,7 @@ module Relaton
23
23
  else
24
24
  [path]
25
25
  end
26
- files.each { |f| tables << YAML.safe_load_file(f, aliases: true) }
26
+ files.each { |f| tables << Yeptris::Psych.safe_load_file(f, aliases: true) }
27
27
  tables
28
28
  end
29
29
 
@@ -1,7 +1,7 @@
1
+ require "yeptris/psych"
1
2
  # encoding: UTF-8
2
3
  # frozen_string_literal: true
3
4
 
4
- require "yaml"
5
5
  require "gb_agencies"
6
6
 
7
7
  module Relaton
@@ -132,7 +132,7 @@ module Relaton
132
132
  # @param pref [String]
133
133
  # @return [Hash{String=>String}]
134
134
  def prefix(pref)
135
- @prefixes ||= YAML.load_file File.join(__dir__, "yaml/prefixes.yaml")
135
+ @prefixes ||= Yeptris::Psych.load_file File.join(__dir__, "yaml/prefixes.yaml")
136
136
  @prefixes[pref]
137
137
  end
138
138
 
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "etc"
2
3
  require "zip"
3
4
  require_relative "../ieee"
@@ -77,7 +78,7 @@ module Relaton
77
78
  # @param file [String]
78
79
  # @return [String, nil]
79
80
  def primary_docid(file)
80
- yaml = YAML.safe_load(File.read(file, encoding: "UTF-8"),
81
+ yaml = Yeptris::Psych.safe_load(File.read(file, encoding: "UTF-8"),
81
82
  permitted_classes: [Symbol, Date, Time])
82
83
  id = yaml && yaml["docidentifier"]&.find do |i|
83
84
  i["type"] == "IEEE" && i["primary"] == true && i["trademark"].nil?
@@ -131,6 +131,12 @@ module Relaton
131
131
  BibXMLParser.pubid_type(id)
132
132
  end
133
133
 
134
+ # The base parser keeps <stream> as a `type="stream"` series, for items
135
+ # with no other slot. This flavor carries the stream in `ext.stream`.
136
+ def stream_series
137
+ []
138
+ end
139
+
134
140
  def contributor # rubocop:disable Metrics/MethodLength
135
141
  contribs = []
136
142
  unless @reference.anchor&.match?(/^I-D/)
@@ -55,9 +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.** 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: `version_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 `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.
59
59
 
60
- **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 `version` to the parse. 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.
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).
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.
61
63
 
62
64
  ### Test Infrastructure
63
65
 
@@ -21,7 +21,7 @@ module Relaton
21
21
  # Pubid::Iho::Identifiers::* (Relaton::Index deserialized it via the
22
22
  # `pubid_class` passed in `#index`), so `pubid_match?` compares pubids.
23
23
  row = index.search(pubid) { |r| pubid_match?(r[:id], pubid) }
24
- .max_by { |r| version_key(r[:id]) }
24
+ .max_by { |r| recency_key(r[:id]) }
25
25
  unless row
26
26
  Util.info "Not found.", key: pubid.to_s
27
27
  return
@@ -96,26 +96,57 @@ module Relaton
96
96
  )
97
97
  end
98
98
 
99
- # The sort key for the latest edition: segments compare as integers,
100
- # so 2.10.0 is later than 2.9.0. A row with no version is the oldest.
101
- def version_key(id)
102
- id.version.to_s.split(".").map(&:to_i)
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].
109
+ def recency_key(id)
110
+ if id.year
111
+ [id.year.to_i]
112
+ elsif id.volume
113
+ [id.volume.to_i, id.issue.to_i]
114
+ else
115
+ id.version.to_s.split(".").map(&:to_i)
116
+ end
103
117
  end
104
118
 
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
124
+
105
125
  # Both `row_id` and `query` are Pubid::Iho::Identifiers::* instances.
106
126
  # Class identity pins the series (S/P/M/B/C). Subdivision keys
107
127
  # (part/appendix/annex/supplement) use strict equality — a nil query
108
128
  # must match a nil row (the umbrella), not an arbitrary subdivision
109
- # under the same (number, version). Only :version stays nil-tolerant: an
110
- # unqualified `IHO B-11` query is expected to find the latest edition.
129
+ # under the same number. That is why this flavor does not use the
130
+ # `===` subset match: pubid reads a nil `part` there as "any part"
131
+ # (pubid's `subset_strict` is not declared for the IHO subdivisions),
132
+ # so `IHO S-100` would answer with a part row.
133
+ #
134
+ # 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.
111
141
  def pubid_match?(row_id, query)
112
142
  row_id.class == query.class &&
113
- row_id.number == query.number &&
114
- row_id.part.to_s == query.part.to_s &&
115
- row_id.appendix.to_s == query.appendix.to_s &&
116
- row_id.annex.to_s == query.annex.to_s &&
117
- row_id.supplement.to_s == query.supplement.to_s &&
118
- (query.version.nil? || row_id.version.to_s == query.version.to_s)
143
+ row_id.exclude(*wildcards(query)) == query.exclude(*wildcards(query))
144
+ end
145
+
146
+ # The discriminators the query leaves unset: wildcards for the
147
+ # search, significant whenever the query states one.
148
+ def wildcards(query)
149
+ DISCRIMINATOR_KEYS.select { |key| query.public_send(key).nil? }
119
150
  end
120
151
  end
121
152
  end
@@ -25,6 +25,12 @@ module Relaton
25
25
  Bibliography.get(code, date, opts)
26
26
  end
27
27
 
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).
30
+ def item_cache_key(pubid, item)
31
+ with_components pubid, version: item.edition&.content
32
+ end
33
+
28
34
  # @param xml [String]
29
35
  # @return [Relaton::Iho::ItemData]
30
36
  def from_xml(xml)
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  module Relaton
2
3
  module Index
3
4
  #
@@ -219,12 +220,12 @@ module Relaton
219
220
  end
220
221
 
221
222
  def load_index(yaml, save = false)
222
- index = YAML.safe_load(yaml, permitted_classes: [Symbol])
223
+ index = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
223
224
  save index if save
224
225
  return deserialize_pubid(index) if check_format(index)
225
226
 
226
227
  report_invalid_index(save, "Wrong structure of")
227
- rescue Psych::SyntaxError
228
+ rescue Yeptris::Psych::SyntaxError
228
229
  report_invalid_index(save, "YAML parsing error when reading")
229
230
  rescue InvalidIndexError
230
231
  report_invalid_index(save, "Wrong structure of")
@@ -430,7 +431,7 @@ module Relaton
430
431
  end
431
432
  yaml = yaml.read unless yaml.is_a?(String)
432
433
  Util.info "Downloaded index from `#{url}` for sqlite materialization", progname
433
- raw = YAML.safe_load(yaml, permitted_classes: [Symbol])
434
+ raw = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
434
435
  return nil unless check_format(raw)
435
436
 
436
437
  Enumerator.new do |y|
@@ -439,7 +440,7 @@ module Relaton
439
440
  y << [number, number, r[:id], r[:file]]
440
441
  end
441
442
  end
442
- rescue Psych::SyntaxError, SocketError, OpenURI::HTTPError, Errno::ECONNRESET,
443
+ rescue Yeptris::Psych::SyntaxError, SocketError, OpenURI::HTTPError, Errno::ECONNRESET,
443
444
  OpenSSL::SSL::SSLError => e
444
445
  Util.info "SQLite index build failed (#{e.message}); falling back", progname
445
446
  nil
@@ -512,8 +513,8 @@ module Relaton
512
513
  return unless yaml
513
514
 
514
515
  begin
515
- raw = YAML.safe_load(yaml, permitted_classes: [Symbol])
516
- rescue Psych::SyntaxError
516
+ raw = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
517
+ rescue Yeptris::Psych::SyntaxError
517
518
  warn_local_index_error("YAML parsing error when reading")
518
519
  delete_sidecar
519
520
  return [[], []]
@@ -613,7 +614,7 @@ module Relaton
613
614
  yaml = zip.entries.first.get_input_stream.read
614
615
  end
615
616
  Util.info "Downloaded index from `#{url}`", progname
616
- raw = YAML.safe_load(yaml, permitted_classes: [Symbol])
617
+ raw = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
617
618
  if check_format(raw)
618
619
  save raw
619
620
  raw = nil # release the parsed copy; read_raw_file loads the sidecar's
@@ -622,7 +623,7 @@ module Relaton
622
623
  warn_remote_index_error "Wrong structure of"
623
624
  [[], []]
624
625
  end
625
- rescue Psych::SyntaxError
626
+ rescue Yeptris::Psych::SyntaxError
626
627
  warn_remote_index_error "YAML parsing error when reading"
627
628
  [[], []]
628
629
  end
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  require "yeptris"
2
3
  require "json"
3
4
  require "net/http"
@@ -152,11 +153,11 @@ module Relaton
152
153
  yaml = unzip(body)
153
154
  raise Error, "empty archive at `#{url(path)}`" unless yaml
154
155
 
155
- index = YAML.safe_load(yaml, permitted_classes: [Symbol])
156
+ index = Yeptris::Psych.safe_load(yaml, permitted_classes: [Symbol])
156
157
  raise Error, "wrong structure of `#{url(path)}`" unless @file_io.check_basic_format(index)
157
158
 
158
159
  @file_io.deserialize_pubid(index)
159
- rescue FileIO::InvalidIndexError, Psych::SyntaxError, Zip::Error, Zlib::Error => e
160
+ rescue FileIO::InvalidIndexError, Yeptris::Psych::SyntaxError, Zip::Error, Zlib::Error => e
160
161
  raise Error, "cannot read `#{url(path)}`: #{e.message}"
161
162
  end
162
163
  end
@@ -88,13 +88,30 @@ module Relaton
88
88
  end
89
89
 
90
90
  def pubid_match?(pubid)
91
- match_excludings = translate_excludings(excludings) + [:all_parts]
92
- match_excludings << :edition unless pubid.typed_stage&.abbr&.include?("DIR")
91
+ match_excludings = base_match_excludings
92
+ unless pubid.typed_stage&.abbr&.include?("DIR")
93
+ match_excludings = match_excludings + [:edition]
94
+ end
93
95
  # Only the candidate is built via .create (from the index) and so may
94
96
  # carry a compound part; `ref_pubid_no_year` is always a parsed pubid,
95
97
  # already split, so it needs no normalization.
96
98
  cand = normalize_compound_part(exclude_id_attrs(pubid, *match_excludings))
97
- cand == exclude_id_attrs(ref_pubid_no_year, *match_excludings)
99
+ cand == ref_excluded(match_excludings)
100
+ end
101
+
102
+ # The candidate's excludings set varies only by the DIR exception; the
103
+ # reference side is invariant per distinct set. Exclude the ref's
104
+ # (possibly deep) base chain once per set, not once per candidate —
105
+ # on a large vocabulary compile this chain ran per candidate per
106
+ # lookup and dominated the bibitem-resolution phase (relaton#423).
107
+ def base_match_excludings
108
+ @base_match_excludings ||= translate_excludings(excludings) + [:all_parts]
109
+ end
110
+
111
+ def ref_excluded(match_excludings)
112
+ @ref_excluded ||= {}
113
+ @ref_excluded[match_excludings] ||=
114
+ exclude_id_attrs(ref_pubid_no_year, *match_excludings)
98
115
  end
99
116
 
100
117
  # @TODO TEMP WORKAROUND (pubid 2.x migration): the v1-generated index
@@ -1,5 +1,5 @@
1
+ require "yeptris/psych"
1
2
  require "fileutils"
2
- require "yaml"
3
3
  require "date"
4
4
 
5
5
  module Relaton::Jcgm
@@ -65,7 +65,7 @@ module Relaton::Jcgm
65
65
  end
66
66
 
67
67
  def load_yaml(file)
68
- YAML.safe_load File.read(file, encoding: "UTF-8"), permitted_classes: [Date, Time]
68
+ Yeptris::Psych.safe_load File.read(file, encoding: "UTF-8"), permitted_classes: [Date, Time]
69
69
  end
70
70
 
71
71
  # "JCGM 17th Meeting (2012)" — ordinal via pubid's Meeting.ordinal.
@@ -1,4 +1,4 @@
1
- require "yaml"
1
+ require "yeptris/psych"
2
2
 
3
3
  module Relaton
4
4
  module Nist
@@ -24,7 +24,7 @@ module Relaton
24
24
  class << self
25
25
  # @return [Hash] the table as written in cswp.yaml
26
26
  def table
27
- @table ||= YAML.load_file File.expand_path("cswp.yaml", __dir__)
27
+ @table ||= Yeptris::Psych.load_file File.expand_path("cswp.yaml", __dir__)
28
28
  end
29
29
 
30
30
  #
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
3
+ require "yeptris/psych"
4
+
4
5
  require "mechanize"
5
6
  require "loc_mods"
6
7
  require_relative "../nist"
@@ -183,7 +184,7 @@ module Relaton
183
184
  end
184
185
 
185
186
  def series
186
- @series ||= YAML.load_file File.expand_path("series.yaml", __dir__)
187
+ @series ||= Yeptris::Psych.load_file File.expand_path("series.yaml", __dir__)
187
188
  end
188
189
  end
189
190
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yeptris/psych"
4
+
3
5
  module Relaton
4
6
  module Sdo
5
7
  # Singleton registry of organizations, built lazily from the index manifest
@@ -35,7 +37,7 @@ module Relaton
35
37
 
36
38
  def build_index
37
39
  yaml = Fetcher.fetch_index
38
- data = yaml.nil? || yaml.empty? ? {} : YAML.safe_load(yaml)
40
+ data = yaml.nil? || yaml.empty? ? {} : Yeptris::Psych.safe_load(yaml)
39
41
  # A corrupt/unexpected index (non-mapping YAML) degrades to "no orgs"
40
42
  # rather than crashing every lookup with a TypeError.
41
43
  organizations = data.is_a?(Hash) ? (data["organizations"] || {}) : {}
@@ -1,3 +1,3 @@
1
1
  module Relaton
2
- VERSION = "3.0.3".freeze
2
+ VERSION = "3.0.5".freeze
3
3
  end
@@ -1,3 +1,4 @@
1
+ require "yeptris/psych"
1
2
  module Relaton
2
3
  module Xsf
3
4
  class Hit < Relaton::Core::Hit
@@ -6,7 +7,7 @@ module Relaton
6
7
 
7
8
  agent = Mechanize.new
8
9
  resp = agent.get hit[:url]
9
- hash = YAML.safe_load resp.body
10
+ hash = Yeptris::Psych.safe_load resp.body
10
11
  hash["fetched"] = Date.today.to_s
11
12
  @doc = Relaton::Xsf::Item.from_yaml hash.to_yaml
12
13
  rescue StandardError => e
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: relaton
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3
4
+ version: 3.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-31 00:00:00.000000000 Z
11
+ date: 2026-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -366,14 +366,14 @@ dependencies:
366
366
  requirements:
367
367
  - - "~>"
368
368
  - !ruby/object:Gem::Version
369
- version: 0.5.174
369
+ version: 0.6.0
370
370
  type: :runtime
371
371
  prerelease: false
372
372
  version_requirements: !ruby/object:Gem::Requirement
373
373
  requirements:
374
374
  - - "~>"
375
375
  - !ruby/object:Gem::Version
376
- version: 0.5.174
376
+ version: 0.6.0
377
377
  - !ruby/object:Gem::Dependency
378
378
  name: leptris
379
379
  requirement: !ruby/object:Gem::Requirement
@@ -408,28 +408,28 @@ dependencies:
408
408
  requirements:
409
409
  - - "~>"
410
410
  - !ruby/object:Gem::Version
411
- version: 1.3.114
411
+ version: 1.3.121
412
412
  type: :runtime
413
413
  prerelease: false
414
414
  version_requirements: !ruby/object:Gem::Requirement
415
415
  requirements:
416
416
  - - "~>"
417
417
  - !ruby/object:Gem::Version
418
- version: 1.3.114
418
+ version: 1.3.121
419
419
  - !ruby/object:Gem::Dependency
420
420
  name: pubid
421
421
  requirement: !ruby/object:Gem::Requirement
422
422
  requirements:
423
423
  - - "~>"
424
424
  - !ruby/object:Gem::Version
425
- version: 2.0.1
425
+ version: 2.0.3
426
426
  type: :runtime
427
427
  prerelease: false
428
428
  version_requirements: !ruby/object:Gem::Requirement
429
429
  requirements:
430
430
  - - "~>"
431
431
  - !ruby/object:Gem::Version
432
- version: 2.0.1
432
+ version: 2.0.3
433
433
  - !ruby/object:Gem::Dependency
434
434
  name: yeptris
435
435
  requirement: !ruby/object:Gem::Requirement
@@ -566,6 +566,8 @@ files:
566
566
  - lib/relaton/bib/converter/bibxml/from_rfcxml_referencegroup.rb
567
567
  - lib/relaton/bib/converter/bibxml/to_rfcxml.rb
568
568
  - lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup.rb
569
+ - lib/relaton/bib/converter/bibxml/to_rfcxml_referencegroup_v3.rb
570
+ - lib/relaton/bib/converter/bibxml/to_rfcxml_v3.rb
569
571
  - lib/relaton/bib/converter/csl.rb
570
572
  - lib/relaton/bib/converter/ris.rb
571
573
  - lib/relaton/bib/converter/titles.rb